【问题标题】:Incomplete types in template code模板代码中的类型不完整
【发布时间】:2014-05-11 07:23:50
【问题描述】:

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

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)
  1. 即使在具有不完整类型的模板类声明(decltype(T())decltype(T{}),但不是简单的T)上也给出error: invalid use of incomplete type 'IncompleteType',而不在代码中使用Test&lt;&gt;::value

  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: '&lt;function-style-cast&gt;' : cannot convert from 'initializer-list' to 'IncompleteType' Source or target has incomplete type


什么编译器符合标准?请注意,std::cout &lt;&lt; typeid(X(IncompleteType)).name() &lt;&lt; std::endl; 之类的简单字符串不会在所有编译器上编译 X 的所有变体(vc++ 除外) X(T) == T)。

【问题讨论】:

  • “X(T) can be T, decltype(T()) or decltype(T{})”是什么意思? X(T)X(T)。怎么可能是Tdecltype(T())decltype(T{})?所有这些都是不同的类型,并且永远不会是相同的函数类型!
  • @JohannesSchaub-litb 假设X(T) 是宏。
  • 如果您不希望它成为一个坏问题,则应将其写在您的问题中
  • 我无法重现代码,但我很好奇如果你这样做会发生什么struct CompleteType { CompleteType() = delete;};
  • (以防万一,用 g++ 的结果仍然是真实的)

标签: c++ templates c++11 incomplete-type


【解决方案1】:

我相信 Clang 和 MSVC 在这种情况下的行为是符合标准的。我认为 GCC 在这里走的是一条捷径。

让我们先把一些事实摆在桌面上。 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-specifier (5.2.2) 操作数的函数调用,否 临时引入,因此上述不适用于 任何此类函数调用的纯右值。 - 结束注释]

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

事实上,这整个问题正是为什么会有一个std::declval 实用程序,因为当你不能使用decltype(T()) 时,你可以使用decltype(std::declval&lt;T&gt;()),而declval 只不过是一个(假)函数返回T 类型的纯右值。但当然,declval 旨在用于不太琐碎的情况,例如decltype( f( std::declval&lt;T&gt;() ) ),其中f 将是一个采用T 类型对象的函数。而declval 不要求类型是完整的(参见第 20.2.4 节)。这基本上是您解决整个问题的方法。

因此,就 GCC 的行为而言,我相信它需要走捷径,因为它试图找出 T()T{} 的类型是什么。我认为只要 GCC 发现 T 指的是类型名称(不是函数名称),它就会推断这是一个构造函数调用,因此,无论查找发现什么是被调用的实际构造函数,返回类型将是T(好吧,严格来说,构造函数没有返回类型,但你明白我的意思)。这里的要点是,这可能是未评估表达式中有用(更快)的捷径。但据我所知,这不是符合标准的行为。

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

请注意,std::cout &lt;&lt; typeid(X(IncompleteType)).name() &lt;&lt; std::endl; 之类的简单字符串不会在所有 X 变体的所有编译器上编译(vc++ 和 X(T) == T 除外)。

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

因此,在这种情况下,T()T{} 案例失败的原因与 decltype 相同(正如我刚刚解释的那样),X(T) == T 案例失败是因为 typeid 需要完整的类型(但 MSVC 设法取消了该要求)。而在 GCC 上,由于typeid 要求所有X(T) 情况的完整类型(即,在sizeoftypeid 情况下,GCC 采用的快捷方式不会影响结果),它会失败。

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

【讨论】:

  • 6 年后,gcc 9.3 仍然存在错误 (live on godbolt.org)。它接受代码,该代码应该会因编译错误而被拒绝。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-11
  • 2016-04-11
  • 2019-06-21
  • 2011-12-14
  • 2013-10-02
相关资源
最近更新 更多