Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Incomplete types in template code

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

  2. 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)   
  1. error C2514: 'IncompleteType' : class has no constructors

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

like image 211
Constructor Avatar asked Mar 31 '14 19:03

Constructor


People also ask

Why is my struct an incomplete type?

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.

What are the two types of templates?

There are two types of templates in C++, function templates and class templates.

How many types of template are there?

There are three kinds of templates: function templates, class templates and, since C++14, variable templates.

Which of the data types are supported by template?

All data types, both primitive and compound types, must be defined by using a template.


1 Answers

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

like image 50
Mikael Persson Avatar answered Sep 21 '22 05:09

Mikael Persson