void_t和带有decltype的尾随返回类型:它们是完全可以互换的吗?

时间:2016-09-17 22:03:45

标签: c++ templates sfinae decltype c++17

考虑以下基于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是否为尾随返回类型加上超载(如果需要)可完全互换?
否则,是什么情况下不能用来解决约束而我被迫使用特定的方法呢?

2 个答案:

答案 0 :(得分:8)

这是元编程的等价物:我应该编写一个函数,还是应该只编写内联代码。更喜欢编写类型特征的原因与更喜欢编写函数的原因相同:它更自我记录,可重用,更容易调试。更喜欢编写尾随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>>;

编写两个尾随返回类型需要写出所有两个表达式...

否定

使用类型特征,可以轻松检查类型 类型是否满足特征。那只是!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 ) { ... }

很好地导致......

标签发送

假设我们有一个短类型特征,那么使用类型特征标记调度会更清楚:

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 ) { ... }

答案 1 :(得分:-1)

当我实现自己的自制的Concepts Lite版本(顺便说一句,我成功)时,我使用了void_t和尾随decltype,这需要创建许多其他类型特征,其中大多数都以某种方式使用Detection惯用法。我使用了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会生成类型的rvalue-ref,而不能分配给它,这也就是我使用它的原因。但其他一切仍然有效,这个版本适用于其他人没有的。

所以现在我的回答是:&#34;只要您的编译器认为它们是&#34;它们就是相同的。这是你必须通过测试找到的东西。我希望在以下版本的MSVC和其他版本中,这将不再是一个问题。