模板代码中的类型不完整

Incomplete types in template code

本文关键字:类型 代码      更新时间:2023-10-16

假设我们有两种类型(完整和不完整):

struct CompleteType{};
struct IncompleteType;

我们还有模板代码:

#include <type_traits>
template <typename = X(T)>
struct Test : std::false_type {};
template <>
struct Test<T> : std::true_type {};

这里T可以是CompleteTypeIncompleteTypeX(T)可以是Tdecltype(T())decltype(T{})(假设X(T)是宏)。

此代码的使用方式如下:

std::cout << std::boolalpha << Test<>::value << std::endl;

下面你可以看到不同的编译器如何处理这样的代码:


clang 3.4

X(T)  T       CompleteType  IncompleteType
T              true          true      
decltype(T())  true          --- (1, 2)
decltype(T{})  true          --- (1, 2)
在代码中不使用Test<>::value的情况下,即使在具有不完整类型的模板类声明上(对于decltype(T())decltype(T{}),但对于简单的T)也会给出error: invalid use of incomplete type 'IncompleteType'
  • 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


    什么编译器符合标准请注意,像std::cout << typeid(X(IncompleteType)).name() << std::endl;这样的简单字符串并不能在所有编译器上编译X的所有变体(vc++X(T) == T除外)。

    我相信Clang和MSVC在这种情况下的行为符合标准。我认为海湾合作委员会在这里走了一条捷径。

    让我们先把一些事实摆在桌面上。decltype表达式的操作数被称为未求值操作数,由于它们最终从未求值,因此处理方式略有不同。

    特别是,对完整类型的要求更少。基本上,如果你有任何临时对象(作为表达式中涉及的函数或运算符中的参数或返回值),它们不需要是完整的(见第5.2.2/11节和第7.1.6.2/5节)。但这只是取消了"你不能声明不完整类型的对象"的通常限制,但并没有取消对不完整类型,即"不能调用不完整类型的成员函数"。这才是关键。

    表达式decltype(T())decltype(T{})(其中T不完整)必须查找类型T的构造函数,因为它是该类的(特殊)成员函数。只是事实上,它是一个构造函数调用,会产生一些歧义(即,它只是在创建一个临时对象吗?还是在调用构造函数?)。如果这是任何其他成员的职能,就不会有任何辩论。幸运的是,该标准确实解决了这场争论:

    12.2/1

    即使临时对象的创建未进行估价(第条5) 或以其他方式避免(12.8),所有语义限制应被尊重,就好像临时对象已经创建以及以后被摧毁。[注意:即使没有调用析构函数或复制/移动构造函数,所有语义限制,例如可访问性(第11条)以及是否删除该功能(8.4.3)。但是,在函数调用用作decltype说明符(5.2.2)的操作数,否引入了临时,因此上述内容不适用于任何此类函数调用的prvalue。-尾注]

    最后一句话可能有点令人困惑,但这只适用于函数调用的返回值。换句话说,如果你有T f();函数,并且你声明了decltype(f()),那么T就不需要是完整的,也不需要对是否有构造函数/析构函数可用和可访问进行任何语义检查。

    事实上,整个问题正是std::declval实用程序的原因,因为当您不能使用decltype(T())时,您可以只使用decltype(std::declval<T>()),而declval只不过是一个返回类型为T的prvalue的(伪)函数。但当然,declval旨在用于不那么琐碎的情况,例如decltype( f( std::declval<T>() ) ),其中f将是接受类型为T的对象的函数。declval并不要求类型是完整的(参见第20.2.4节)。这基本上就是解决整个问题的方法。

    因此,就GCC的行为而言,我认为它在试图弄清楚T()T{}的类型时走了一条捷径。我认为,一旦GCC发现T指的是类型名(而不是函数名),它就会推断这是一个构造函数调用,因此,无论查找结果如何,返回类型将是T(严格来说,构造函数没有返回类型,但您理解我的意思)。这里的重点是,在未求值的表达式中,这可能是一个有用的(更快的)捷径。但据我所知,这不是符合标准的行为。

    如果GCC允许CompleteType的构造函数被删除或私有,那么这也与上面引用的标准段落直接矛盾。在这种情况下,编译器需要强制执行所有语义限制,即使表达式没有求值。

    注意,像std::cout << typeid(X(IncompleteType)).name() << std::endl;这样的简单字符串不会在所有编译器上编译X的所有变体(除了vc++和X(T)==T)。

    这是预期的(MSVC和X(T)==T除外)。typeidsizeof运算符与decltype的相似之处在于,它们的操作数是未赋值的,但它们都有一个额外的要求,即结果表达式的类型必须是完整类型。可以想象,编译器可以为不完整的类型(或者至少具有部分类型信息)解析typeid,但该标准需要一个完整的类型,因此编译器不必这样做。我想这就是微软风投正在做的事情。

    因此,在这种情况下,T()T{}的失败原因与decltype相同(正如我刚才解释的),而X(T) == T的失败原因是typeid需要一个完整的类型(但MSVC设法满足了这一要求)。在GCC上,它失败是因为typeid需要所有X(T)病例的完整类型(即,在sizeoftypeid的情况下,GCC采取的捷径不影响结果)。

    所以,总的来说,我认为Clang是三者中最符合标准的(不走捷径或进行扩展)。