Search code examples
c++templatesone-definition-rule

Different template instantiations in multiple cpp files


I recently stumbled upon a curious cpp template architecture. The template class declaration was in a header file, and the definition in a cpp file, with some explicit instantiations. Not as common as full template definition in header, but still not that surprising. But there was actually multiple cpp files with different declarations for the same template class, but with different explicit instantiations.

A small reproducible example would look like this:

// header.hpp
template <typename T>
struct Foo {
    T foo(T t);
};
// impl1.cpp
#include "header.hpp"

template <typename T>
T Foo<T>::foo(T t) {
    return t + 1;
}
template struct Foo<int>;
template struct Foo<short>;
// impl2.cpp
#include "header.hpp"

template <typename T>
T Foo<T>::foo(T t) {
    return t + 0.1;
}
template struct Foo<float>;
template struct Foo<double>;
// main.cpp
#include "header.hpp"
#include <iostream>

int main() {
    std::cout << Foo<int>().foo(1) << std::endl;
    std::cout << Foo<short>().foo(1) << std::endl;
    std::cout << Foo<double>().foo(1.2) << std::endl;
}

And the result is

2
2
1.3

At first I thought it would be a ODR violation but it seems that both GCC and Clang compile it without even a warning. Is it ok because each Foo instance is a different class so no "real" class is defined twice? In this case, if I add Foo<int> to both cpp files, it should break the ODR, but both compilers are still ok with it (using the one in impl1 it seems).

I found this question that looks similar but is actually sort of the opposite: defining only parts of the class in different cpp files but for the same types, while here the class is entirely redefined for different types in each cpp file.

My question is: is this valid C++ code? If so, is it a common technique or quite a niche usecase? And if not, is it that hard to detect that both compilers fail to see it, or is it some kind of extensions over the C++ standard?


Solution

  • Defining T Foo<T>::foo(T t) in two different files with a different function body is an ODR violation. Specifically, it violates [basic.def.odr] p15.4. The definitions have to consist of the same tokens, and 1 and 0.1 are obviously not the same token.

    Keep in mind that these inter-TU ODR-violations make the program ill-formed, no diagnostic required (IFNDR), so your compiler isn't required to give you an error (and typically doesn't). Your code appears to work, but it's not valid C++.

    You could define everything in a header and switch behavior with if constexpr for integers and floating-point. You could also split off explicit specializations into source files:

    template <> // in a source file
    int Foo<int>::foo(int t) {
        return t + 1;
    }
    

    Yet another correct option is to define partial specializations of Foo for integral types, or make two version of foo with different requires-clauses:

    template <typename T>
    struct Foo {
        T foo(T t) requires std::integral<T>;
        T foo(T t) requires std::floating_point<T>;
    };
    

    Anyhow, there are lots of correct options.