void_t 和尾部 return 类型与 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() { }

同样可以使用尾随 return 类型和 decltype:

template<typename T>
auto func() -> decltype(std::declval<T>().foo(), void()) { }

我想到的所有例子都是如此。我没有找到可以使用 void_t 或带有 decltype 的尾部 return 类型而其对应的类型不能使用的情况。
复杂 的情况可以通过尾随 return 类型和重载的组合来解决(例如,当 检测器 是用于在两个功能之间切换而不是作为禁用或启用某些功能的触发器)。

是这样吗?它们(void_tdecltype 作为尾随 return 类型加上需要时的重载)是否完全可以互换?
否则,在什么情况下不能使用一个来解决约束而我被迫使用特定方法?

这是元编程的等价物:我应该编写一个函数还是应该直接编写内联代码。喜欢写一个类型特征的原因和喜欢写一个函数的原因是一样的:它更自我记录,它是可重用的,它更容易调试。喜欢编写尾随 decltype 的原因与喜欢编写内联代码的原因类似:它是一次性的,不可重用,所以为什么要努力将其分解并为它想出一个合理的名称?

但是这里有一堆你可能想要类型特征的原因:

重复

假设我有一个特征我想检查很多次。喜欢fooable。如果我写一次类型特征,我可以将其视为一个概念:

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>>;

组合两个尾随 return 类型需要写出所有两个表达式...

否定

有了类型特征,很容易检查类型满足特征。那只是 !fooable<T>::value。您不能编写 trailing-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 ) { ... }

这很好地引导到...

标签发送

假设我们有一个 short 类型特征,用类型特征标记 dispatch 会更清楚:

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!");
};

概念

当(如果?)我们得到概念时,显然当涉及到与元编程相关的一切时,实际使用概念要强大得多:

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 产生了类型的右值引用,不能分配给它,这就是我使用它的原因。但其他一切仍然有效,这个版本在其他人没有的地方工作。

所以现在我的答案是:"they're identical, as long as your compiler thinks they are"。这是您必须通过测试找出的东西。我希望这将不再成为 MSVC 和其他版本的后续版本中的问题。