Suppose we have two types (complete and incomplete):
struct CompleteType{};
struct IncompleteType;
Also we have template code:
#include <type_traits>
template <typename = X(T)>
struct Test : std::false_type {};
template <>
struct Test<T> : std::true_type {};
T
can be CompleteType
or IncompleteType
here and X(T)
can be T
, decltype(T())
or decltype(T{})
(suppose X(T)
is a macro).
This code is used in the following manner:
std::cout << std::boolalpha << Test<>::value << std::endl;
Below you can see how different compilers deal with such code:
clang 3.4
X(T) \ T CompleteType IncompleteType
T true true
decltype(T()) true --- (1, 2)
decltype(T{}) true --- (1, 2)
error: invalid use of incomplete type 'IncompleteType'
is given even on template class declarations with incomplete types (both for decltype(T())
and decltype(T{})
, but not for simple T
) without using Test<>::value
in the code.
error: too few template arguments for class template 'Test'
g++ 4.8.1
X(T) \ T CompleteType IncompleteType
T true true
decltype(T()) true true
decltype(T{}) true true
vc++ 18.00.21005.1
X(T) \ T CompleteType IncompleteType
T true true
decltype(T()) true --- (1)
decltype(T{}) true --- (2)
error C2514: 'IncompleteType' : class has no constructors
error C2440: '<function-style-cast>' : cannot convert from 'initializer-list' to 'IncompleteType' Source or target has incomplete type
What compiler acts in accordance with standard? Note that simple string like std::cout << typeid(X(IncompleteType)).name() << std::endl;
does not compile on all compilers for all variants of X
(except for vc++ and X(T) == T
).
An incomplete type is a type that describes an identifier but lacks information needed to determine the size of the identifier. An incomplete type can be: A structure type whose members you have not yet specified. A union type whose members you have not yet specified.
There are two types of templates in C++, function templates and class templates.
There are three kinds of templates: function templates, class templates and, since C++14, variable templates.
All data types, both primitive and compound types, must be defined by using a template.
I believe that the behavior of Clang and MSVC are consistent with the standard in this situation. I think GCC is taking a bit of a short-cut here.
Let's put a few facts on the table first. The operand of a decltype
expression is what is called an unevaluated operand, which are treated a bit differently due to fact that they are ultimately never evaluated.
Particularly, there are fewer requirements about the types being complete. Basically, if you have any temporary object (as parameters or return values in the functions or operators involved in the expression), they are not required to be complete (see Sections 5.2.2/11 and 7.1.6.2/5). But this only lifts the usual restriction of "you cannot declare an object of an incomplete type", but it does not lift the other restriction on incomplete types, which is that "you cannot call a member function of an incomplete type". And that's the kicker.
The expression decltype(T())
or decltype(T{})
, where T
is incomplete, must necessarily look-up the constructor(s) of the type T
, as it's a (special) member function of that class. It's only the fact that it's a constructor call that creates a bit of an ambiguity (i.e., Is it just creating a temporary object? Or is it calling a constructor?). If it was any other member function, there would be no debate. Fortunately, the standard does settle that debate:
12.2/1
Even when the creation of the temporary object is unevaluated (Clause 5) or otherwise avoided (12.8), all the semantic restrictions shall be respected as if the temporary object had been created and later destroyed. [ Note: even if there is no call to the destructor or copy/move constructor, all the semantic restrictions, such as accessibility (Clause 11) and whether the function is deleted (8.4.3), shall be satisfied. However, in the special case of a function call used as the operand of a decltype-specifier (5.2.2), no temporary is introduced, so the foregoing does not apply to the prvalue of any such function call. - end note ]
The last sentence might be a bit confusing, but that only applies to the return-value of a function call. In other words, if you have T f();
function, and you declare decltype(f())
, then T
is not required to be complete or have any semantic checks on whether there is a constructor / destructor available and accessible for it.
In fact, this whole issue is exactly why there is a std::declval
utility, because when you cannot use decltype(T())
, you can just use decltype(std::declval<T>())
, and declval
is nothing more than a (fake) function that returns a prvalue of type T
. But of course, declval
is intended to be used in less trivial situations, such as decltype( f( std::declval<T>() ) )
where f
would be a function taking an object of type T
. And declval
does not require that the type is complete (see Section 20.2.4). This is basically the way you get around this whole problem.
So, as far as GCC's behavior is concerned, I believe that it takes a short-cut as it attempts to figure out what the type of T()
or T{}
is. I think that as soon as GCC finds that T
refers to a type name (not a function name), it deduces that this is a constructor call, and therefore, regardless of what the look-up finds as the actual constructor being called, the return type will be T
(well, strictly speaking constructors don't have a return type, but you understand what I mean). The point here is that this could be a useful (faster) short-cut in an unevaluated expression. But this is not standard-compliant behavior, as far as I can tell.
And if GCC allows for CompleteType
with the constructor either deleted or private, then that is also in direct contradiction with the above-quoted passage of the standard. The compiler is required to enforce all semantic restrictions in that situation, even if the expression is not evaluated.
Note that simple string like
std::cout << typeid(X(IncompleteType)).name() << std::endl;
does not compile on all compilers for all variants of X (except for vc++ and X(T) == T).
This is expected (except for MSVC and X(T) == T). The typeid
and sizeof
operators are similar to decltype
in the sense that their operands are unevaluated, however, both of them have the additional requirement that the type of the resulting expression must be a complete type. It is conceivable that a compiler could resolve typeid
for incomplete types (or at least, with partial type-info), but the standard requires a complete type such that compilers don't have to do this. I guess this is what MSVC is doing.
So, in this case, the T()
and T{}
cases fail for the same reason as for decltype
(as I just explained), and the X(T) == T
case fails because typeid
requires a complete type (but MSVC manages to lift that requirement). And on GCC, it fails due to typeid
requiring a complete type for all the X(T)
cases (i.e., the short-cut GCC takes doesn't affect the outcome in the case of sizeof
or typeid
).
So, all in all, I think that Clang is the most standard-compliant of the three (not taking short-cuts or making extensions).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With