Void_t和带有decltype的尾随返回类型是完全可互换的吗?
void_t and trailing return type with decltype: are they completely interchangeable?
考虑以下基于void_t
的基本示例:
template<typename, typename = void_t<>>
struct S: std::false_type {};
template<typename T>
struct S<T, void_t<decltype(std::declval<T>().foo())>>: std::true_type {};
可以这样使用:
template<typename T>
std::enable_if_t<S<T>::value> func() { }
同样可以使用尾随返回类型和decltype
:
template<typename T>
auto func() -> decltype(std::declval<T>().foo(), void()) { }
我想到的所有例子都是这样。我未能找到void_t
或带有decltype
的尾随返回类型可以使用而其对应类型不能使用的情况。
最复杂的情况可以通过尾随返回类型和重载的组合来解决(例如,当检测器用于在两个函数之间切换而不是作为禁用或启用某些东西的触发器时)。
是这样吗?它们(void_t
和decltype
作为尾部返回类型,如果需要的话再加上重载)是完全可互换的吗?
否则,在什么情况下,不能使用一个来绕过约束,我被迫使用特定的方法?
这是元编程中的等价问题:我应该编写函数还是应该内联编写代码。喜欢写类型trait的原因和喜欢写函数的原因是一样的:它更具有自文档性,更易于重用,更易于调试。喜欢写尾部decltype的原因与喜欢写内联代码的原因类似:它是一次性的,不能重用,所以为什么要花时间把它分解出来,并为它取一个合理的名字呢?
但是这里有一堆你可能需要类型特征的原因:
重复假设我有一个特性,我想检查很多次。比如fooable
。如果我只写一次类型trait,我可以把它当作一个概念:
template <class, class = void>
struct fooable : std::false_type {};
template <class T>
struct fooable<T, void_t<decltype(std::declval<T>().foo())>>
: std::true_type {};
现在我可以在很多地方使用同样的概念:
template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void bar(T ) { ... }
template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void quux(T ) { ... }
对于检查多个表达式的概念,您不希望每次都重复检查。
可组合性
随着重复,组合两种不同的类型特征很容易:
template <class T>
using fooable_and_barable = std::conjunction<fooable<T>, barable<T>>;
组合两个尾随返回类型需要写出所有的表达式…
否定使用类型trait,很容易检查类型是否满足trait。这就是!fooable<T>::value
。您不能编写尾随- decltype
表达式来检查某些内容是否无效。当你有两个不相交的重载时,可能会出现这种情况:
template <class T, std::enable_if_t<fooable<T>::value>* = nullptr>
void bar(T ) { ... }
template <class T, std::enable_if_t<!fooable<T>::value>* = nullptr>
void bar(T ) { ... }
很好地引出……
标记分派
假设我们有一个简短的类型trait,用类型trait标记分派会更清晰:
template <class T> void bar(T , std::true_type fooable) { ... }
template <class T> void bar(T , std::false_type not_fooable) { ... }
template <class T> void bar(T v) { bar(v, fooable<T>{}); }
template <class T> auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
template <class T> void bar(T v, ... ) { ... }
template <class T> void bar(T v) { bar(v, 0); }
0
和int/...
有点奇怪,对吧?
static_assert
如果我不想对一个概念进行SFINAE,而只是想用一个明确的信息来硬失败,该怎么办?
template <class T>
struct requires_fooability {
static_assert(fooable<T>{}, "T must be fooable!");
};
概念当(if?)我们得到概念时,显然,当涉及到与元编程相关的一切时,实际使用概念要强大得多:
template <fooable T> void bar(T ) { ... }
当我实现我自己的自制版本的Concepts Lite时(顺便说一下,我成功了),我使用了void_t和尾部decltype,这需要创建许多额外的类型特征,其中大多数都以某种方式使用了检测习惯用法。我使用了void_t,后面是decltype,前面是decltype。
据我所知,这些选项在逻辑上是等价的,所以一个理想的、100%兼容的编译器应该使用所有这些选项产生相同的结果。然而,问题是特定的编译器可能(也将)在不同的情况下遵循不同的实例化模式,其中一些模式可能远远超出了内部编译器的限制。例如,当我试图使MSVC 2015 Update 2 3检测相同类型乘法的存在时,唯一有效的解决方案是前面的decltype:
template<typename T>
struct has_multiplication
{
static no_value test_mul(...);
template<typename U>
static decltype(*(U*)(0) *= std::declval<U>() * std::declval<U>()) test_mul(const U&);
static constexpr bool value = !std::is_same<no_value, decltype(test_mul(std::declval<T>())) >::value;
};
其他版本都会产生内部编译器错误,尽管其中一些在Clang和GCC中工作得很好。我还必须使用*(U*)(0)
而不是declval
,因为在一行中使用三个declval
,虽然完全合法,但在这种特殊情况下对编译器来说太多了。
*(U*)(0)
,因为declval
产生的右值-ref类型,不能被赋值,这就是我使用这个的原因。但是其他的都是有效的,这个版本在其他版本没有的地方起作用了。
所以现在我的答案是:"它们是相同的,只要你的编译器认为它们是"。这是你必须通过测试来发现的。我希望在接下来的MSVC和其他版本中这将不再是一个问题。
- 如何获取std::result_of函数的返回类型
- 奇怪的结构&GCC&clang(void*返回类型)
- 如何建立使用模板函数的lambda函数的尾部返回类型
- 为什么与常规GCC不同,即使有"学究性错误",MinGW-GCC也能容忍丢失的返回类型
- 在没有定义返回类型的函数中返回布尔值,并将结果保存在无错误的char编译中-为什么
- 特征::矩阵<双精度,1,3> 结构类型函数中的返回类型函数
- 函数作为模板参数,是否对返回类型强制约束
- C++中函数的向量返回类型引发错误
- 检查函数返回类型是否与STL容器类型值相同
- 为什么返回类型中需要typename?C++
- <Windows>为什么 std::thread::native_handle 返回类型为"long long unsigned int"的值,而不是 void*(又名 HANDLE)?
- 警告:在函数返回类型 [-Wignore 限定符] 时忽略类型限定符
- 为什么 c++(g++) 不允许模板返回类型和函数名称之间有空格?
- 为什么协程的返回类型必须是可移动构造的?
- 可互换使用的类型和非类型模板参数
- 仅通过指针强制转换使类类型可互换,而无需分配任何新对象
- 使用方法捕获模板化可调用对象的返回类型
- std::函数,返回任何可求和类型
- 在VS2010中获取可调用类型的返回类型
- Void_t和带有decltype的尾随返回类型是完全可互换的吗?