Void_t和带有decltype的尾随返回类型是完全可互换的吗?

void_t and trailing return type with decltype: are they completely interchangeable?

本文关键字:返回类型 可互换 decltype Void      更新时间:2023-10-16

考虑以下基于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_tdecltype作为尾部返回类型,如果需要的话再加上重载)是完全可互换的吗?
否则,在什么情况下,不能使用一个来绕过约束,我被迫使用特定的方法?

这是元编程中的等价问题:我应该编写函数还是应该内联编写代码。喜欢写类型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); }

0int/...有点奇怪,对吧?

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和其他版本中这将不再是一个问题。