c++templatesconstructorlanguage-lawyer

Substituting template parameter with its known value into a typedef is inconsistent between compilers


The code is not an ideal implementation, but it demonstrates something interesting.

I have a class template with some bool parameters, where specializations of the class have slightly different functionality. One specialzation is illegal. I initially chose the "main" template definition to be the specialization where all of these parameters were false.

Each legal specialization has a typedef this_type which is the same class as the specialization itself. Because the "main" template has all of the values false, I chose to use false in the typedef instead of the template parameter names. The static_assert guarantees that the template parameters are all false, and this_type really is the same type.

All specializations have defaulted copy and move constructors with this_type in the signature (yes, you can just use the template name, please bear with me).

clang compiles this code without complaint, but gcc and MSVC both give a "cannot be defaulted" error.

#include <string>
#include <limits>

template <class T, bool ISNUM = std::numeric_limits<T>::is_specialized, bool ISINT = std::numeric_limits<T>::is_integer>
class foo
{
    static_assert(!(ISNUM || ISINT || std::numeric_limits<T>::is_specialized || std::numeric_limits<T>::is_integer));
    typedef foo<T, false, false> this_type;
    // typedef foo <T, ISNUM, ISINT> this_type;  ALWAYS WORKS
    T v;
    public:
    foo (T iv): v (std::move (iv)) {}
    foo (this_type const &) = default; // HERE
    foo (this_type &&) = default;      // ALSO HERE
  void nominal_thing();
};

template <class T>
class foo<T, false, true>
{
    static_assert(std::numeric_limits<T>::is_integer && !std::numeric_limits<T>::is_specialized, "don't mess with constraints");
    static_assert(!std::numeric_limits<T>::is_integer, "this specialization is forbidden");
};

template <class T>
class foo<T, true, false>
{
    static_assert(std::numeric_limits<T>::is_specialized && !std::numeric_limits<T>::is_integer, "don't mess with constraints");
    typedef foo<T, true, false> this_type;
    T v;
    public:
    foo (T iv): v (std::move (iv)) {}
    foo (this_type const &) = default;
    foo (this_type &&) = default;
    void ratio_thing();
};


template <class T>
class foo<T, true, true>
{
    static_assert(   std::numeric_limits<T>::is_specialized
                  && std::numeric_limits<T>::is_integer, 
                 "don't mess with constraints");
    typedef foo<T, true, true> this_type;
    T v;
    public:
    foo (T iv): v (std::move (iv)) {}
    foo (this_type const &) = default;
    foo (this_type &&) = default;
    void integer_thing();
};

using namespace std::string_literals;

int main()
{
    foo<double> fd {20} ;
    foo<std::string> fs {"a string"s} ;
}

There's a better way to implement this, but it doesn't demonstrate the issue: Should the the compiler recognize the this_type typedef in the "main" template body as a typedef of itself?


Solution

  • Clang is right. In GCC, this is bug 86646.

    This boils down to the question of whether, in order for a constructor to be a copy/move constructor, the compiler needs to be able to identify it as such before even instantiating the enclosing class. If the answer were "yes", then constructs such as

    template <class T>
    struct Metafunction { /* ... */ };
    
    template <class T>
    struct S {
        S(const S<typename Metafunction<T>::type>&) = default;
    };
    

    would need to be illegal, because the compiler cannot determine whether typename Metafunction<T>::type is always going to be T, therefore it cannot identify the constructor declaration in the template S as being a copy constructor.

    However, the actual answer to the question is "no". What the above actually does is declare a constructor that may or may not be a copy constructor, depending on the argument for T (to be specific, depending on whether typename Metafunction<T>::type actually does turn out to be the same as the argument for T). If you instantiate S with a type for which that isn't so, then the compiler is allowed to reject that specialization, because in that specialization, there's a defaulted function that is not a kind of function that can be defaulted.

    The only requirements in the standard for a function to be a copy or move constructor are found in [class.copy.ctor]:

    A non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, and either there are no other parameters or else all other parameters have default arguments ([dcl.fct.default]).

    A non-template constructor for class X is a move constructor if its first parameter is of type X&&, const X&&, volatile X&&, or const volatile X&&, and either there are no other parameters or else all other parameters have default arguments ([dcl.fct.default]).

    There are no restrictions on how you can "spell" the parameter type if you want your constructor to still be considered a copy/move constructor.