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?
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 typeX&
,const X&
,volatile X&
orconst 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 typeX&&
,const X&&
,volatile X&&
, orconst 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.