模板代码中的类型不完整
Incomplete types in template code
假设我们有两种类型(完整和不完整):
struct CompleteType{};
struct IncompleteType;
我们还有模板代码:
#include <type_traits>
template <typename = X(T)>
struct Test : std::false_type {};
template <>
struct Test<T> : std::true_type {};
这里T
可以是CompleteType
或IncompleteType
,X(T)
可以是T
、decltype(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)
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
什么编译器符合标准请注意,像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除外)。typeid
和sizeof
运算符与decltype
的相似之处在于,它们的操作数是未赋值的,但它们都有一个额外的要求,即结果表达式的类型必须是完整类型。可以想象,编译器可以为不完整的类型(或者至少具有部分类型信息)解析typeid
,但该标准需要一个完整的类型,因此编译器不必这样做。我想这就是微软风投正在做的事情。
因此,在这种情况下,T()
和T{}
的失败原因与decltype
相同(正如我刚才解释的),而X(T) == T
的失败原因是typeid
需要一个完整的类型(但MSVC设法满足了这一要求)。在GCC上,它失败是因为typeid
需要所有X(T)
病例的完整类型(即,在sizeof
或typeid
的情况下,GCC采取的捷径不影响结果)。
所以,总的来说,我认为Clang是三者中最符合标准的(不走捷径或进行扩展)。
- 处理小于cpu数据总线的数据类型.(c++转换为机器代码)
- 类没有命名C++代码中的类型错误
- 当无法使用模板和宏时,生成类型变体C++代码的最简单方法是什么?
- 扩展C++生成的代码的模板参数类型名称
- 有没有办法通过使用十进制 ASCII 代码自动类型扣除来获取字符?
- 我在 C++ 代码中遇到错误警告:控制到达非空函数 [-Wreturn 类型] 的末尾
- 标准对此指向成员函数类型模板参数有何说明?是我的代码有误,还是 MSVS 16.6 有问题?
- 提供对不同类型的数据(建议、代码审查)的线程安全访问的类
- 当前不会命中断点。没有调试器目标代码类型的可执行代码与此文件关联
- 我的代码中是否有任何类型的错误,因为它没有给出正确的输出
- 用于C++代码的 API 监视器类型定义 (XML)
- 避免使用 std::any 编写相同的重复类型检查代码
- turbo 为什么我的C++代码中出现错误类型名称预期"错误?
- 具有相同数据类型代码的相同逻辑代码在 Java 中传递,但不在 C++ 中传递?
- 如何在数组类型的模板代码中计算std::size_t
- 将-Wtype限制与类型泛型代码一起使用
- 将fwrite转换为C++类型代码以写入二进制文件
- 当没有浮点数据类型时,为什么此代码会出现浮点异常
- Cython:如何从C级类型获取'actual Python type'(类型代码/dtype)
- 如何生成 cv::Mat 类型代码