Search code examples
c++templatessfinaetemplate-specializationtemplate-instantiation

Why do partial and full C++ template specializations, that look almost the same, produce different results?


I haven't written many C++ templates till recently, but I'm trying to take a deep dive into them. Fixing one developer's code I managed to produce some compiler behavior that I can't explain. Here is the simplified program. ( When I try to simplify more, I lose that "strange" behavior). Let's think of some class Depender, which is a parameter of template struct Dependency. Depender can depend on the List of Dependees. I have some macros that can produce specializations of the Dependency template. Dots in the following code block stand for the possible macro expansion. I have a forward-declared MainDependee before dots, and MainDependee's definition after dots.

#include <type_traits>

template <typename T, typename = void>
struct IsCompleteType : std::false_type
{};

template <typename T>
struct IsCompleteType<T, std::enable_if_t<( sizeof( T ) > 0 )>> : std::true_type
{};

template<template<typename...> class List, typename Dependee>
struct DependencyInternal
{
    using Type = std::conditional_t<IsCompleteType<Dependee>::value, Dependee, Dependee>;
};

template<typename... Ts> class StubList;

class MainDependee;  // forward declaration
class MainDepender
{};

template<template<typename...> class List, typename Depender>
struct Dependency;

....... //here is the specialization

class MainDependee {};
int main()
{
   Dependency<StubList, MainDepender> a;
}

When the dots are replaced with

template<template<typename... Us> class List>
struct Dependency<List, MainDepender>
{
    using Type = typename DependencyInternal<List, MainDependee>::Type;
};

then in main I get IsCompleteType<MainDependee>::value == true.

But when the dots are replaced with

template<>
struct Dependency<StubList, MainDepender>
{
    using Type = typename DependencyInternal<StubList, MainDependee>::Type;
};

then IsCompleteType<MainDependee>::value == false.

Please tell me, what rule describes the difference between these options?

Try the code yourself


Solution

  • An explicit (full) specialization is not itself a templated entity and all name lookup etc., as well as ODR, is done for it as if it wasn't a template. MainDependee is not complete at the point you wrote it and therefore IsCompleteType<MainDependee> is inherited from std::false_type at this point.

    A partial specialization is itself a templated entity and will follow the rules for templates. In particular a definition will be instantiated from the partial specialization only when and where an instantiation is required. In this case the instantiation for Dependency<StubList, MainDepender> has its point of instantiation directly before main where MainDependee is complete. Only there Type is computed (because the right-hand side is dependent on the template parameter) and IsCompleteType<MainDependee> instantiated, so that it will be inherited from std::true_type.

    Note that class template specializations are instantiated only once per translation unit. If you use Dependency<StubList, MainDepender>::value multiple times in a translation unit, it will always result in the same value, the one which would be correct at the first point from where instantiation is required.

    Furthermore, if Dependency<StubList, MainDepender>::value or IsCompleteType<MainDependee> would have different values in different translation units because of different relative placement of MainDependee, your program will be IFNDR (ill-formed, no diagnostic required), effectively meaning it will have undefined behavior.

    It is not possible to use a type trait like this to safely check whether a type is complete. For the same reason there is no such trait in the standard library.