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).