Search code examples
c++templatestemplate-meta-programming

Member function implemented outside of the class body when using template metaprogramming


I've got a templated class that uses a template parameter (std::enable_if_t) to restrict the main template parameter as shown here:

template <typename T, typename
 = std::enable_if_t<std::is_arithmetic_v<T>>>
class Foo 
{
void foo();
}

template<typename T>
void Foo<T>::foo() {} // compiler complains

I know I could work around this using static_assert, but I'd like to know what the correct approach would be in this case.

I suppose I could simply copy both template parameters again, but that seems very odd to me.


Solution

  • You have to add the second template parameter:

    // ..................vvvvvvvvvv
    template<typename T, typename U>
    void Foo<T, U>::foo() {} // compiler doesn't complains anymore
    // ........^^
    

    or, maybe better, implement the method inside the class:

    template <typename T, typename
     = std::enable_if_t<std::is_arithmetic_v<T>>>
    class Foo 
    {
    void foo() {}
    // .......^^^
    };
    

    Anyway... take into account that your way to use SFINAE to restrict the type of the (first) parameter has a defect: you are operating over the default value of the second template parameter, so it can be bypassed by explicitly including the second template parameter in instantiations.

    I mean... you can correctly define a type when T is arithmetic:

    Foo<int> f; // compile
    

    and correctly doesn't compile when T isn't arithmetic:

    Foo<void> f; // compilation error
    

    But, it compiles (when T isn't arithmetic) when you explicitly include a second template parameter:

    Foo<void, void>  f; // compiles!!!
    

    I suppose you don't wont that Foo<void, void> to compile.

    To avoid this problem, you can use SFINAE on the left side of the equal sign, deleting the template parameter, not the default value.

    For example, you can write:

    template <typename T,
              std::enable_if_t<std::is_arithmetic_v<T>, bool> = true>
    class Foo 
    {
    void foo() {}
    };
    

    This way, you're sure nobody can instantiate a Foo object when the first template parameter isn't an arithmetic type.

    This solution has a little problem (or maybe not... depends on what you expect): you can define two different classes over the same T parameter.

    I mean... you can define:

    Foo<int>        f0;
    Foo<int, true>  f1;  // same type as f0
    Foo<int, false> f2;  // different type from f0 and f1
    

    To avoid this, for the type of the second parameter, you can use a type that accepts a single value. For example: std::nullptr_t

    template <typename T, 
              std::enable_if_t<std::is_arithmetic_v<T>, std::nullptr_t> = nullptr>
    class Foo 
    {
    void foo() {}
    };
    

    Now, you can have a Foo<int>, that is shorthand for Foo<int, nullptr>, but not a class that differs in the second template parameter.