问题 如何在C ++ 11,C ++ 11,14和17之前简化复杂的SFINAE语法?


这个问题的灵感来自于 这个答案。我想知道在给定标准中简化它的最佳方法是什么。我知道并且个人使用/仍在使用,因为C ++ 14是宏 REQUIRES(x)

定义:

template<long N>
struct requires_enum
{
    enum class type
    {
        none,
        all
    };
};

#define REQUIRES(...) requires_enum<__LINE__>::type = \
                      requires_enum<__LINE__>::type::none, \
                      bool PrivateBool = true, \
                      typename std::enable_if<PrivateBool && (__VA_ARGS__), int>::type = 0

即使对于非模板化的函数调用也要使用:

template<REQUIRES(sizeof(int)==4)>
int fun() {return 0;}

int main()
{ 
    fun(); //only if sizeof(int)==4
}

原本的 REQUIRES 我用的就是这个 岗位

还有什么好的技巧?


SFINAE的一些例子需要一些或很长时间才能理解读者刚开始使用SFINAE的冒险:

Pre-C ++ 11 SFINAE示例(资源):

template <typename T>
struct has_typedef_foobar {
    // Types "yes" and "no" are guaranteed to have different sizes,
    // specifically sizeof(yes) == 1 and sizeof(no) == 2.
    typedef char yes[1];
    typedef char no[2];

    template <typename C>
    static yes& test(typename C::foobar*);

    template <typename>
    static no& test(...);

    // If the "sizeof" of the result of calling test<T>(nullptr) is equal to sizeof(yes),
    // the first overload worked and T has a nested type named foobar.
    static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
};

2948
2017-09-27 14:31


起源

这看起来很酷! - vsoftco
老实说,为什么不呢 typename std::enable_if< sizeof(int)==4, int > =0?啊,它不依赖于模板参数。为什么 requires_enum?签名碰撞? - Yakk - Adam Nevraumont
@查看我在示例下面添加的帖子的链接。 - xinaiz
所以, REQUIRES 条款 fun 写在一个8字节的系统上 int 在需要静态错误的系统上生成一个不正确的程序,不需要诊断,如果正文取决于4字节的 int 要有良好的形式。这是因为没有一组模板参数值可以传递,这将使函数体变得有效。如果您尝试使用它来根据模板类参数禁用模板类的模板成员,则标准中是否会出现类似问题尚不清楚。 - Yakk - Adam Nevraumont


答案:


如果您正在使用C ++ 11(示例代码包含 std::enable_if,所以我猜这是这种情况)或连续修订,我会用 static_assert 在这种情况下:

int fun() {
    static_assert(sizeof(int)==4, "!");
    return 0;
}

int main() {
    fun();
}

你没有一套 功能 从中挑选一个工作的。
正如我曾经说过的那样,这更像是一个 替换失败总是一个错误 比一个 替换失败不是错误
你想要的是一个编译时触发器和一个 static_assert 用温和的错误信息做到这一点。

当然,它也比复杂的sfinae表达更容易阅读!!


如果要在两个函数之间进行选择而不想使用模板机制或宏,请不要忘记重载是语言的一部分(前C ++ 11工作示例):

#include <iostream>

template<bool> struct tag {};
int fun(tag<true>) { return 0; } 
int fun(tag<false>) { return 1; }
int fun() { return fun(tag<sizeof(int) == 4>()); }

int main() {
    std::cout << fun() << std::endl;
}

这可以很容易地扩展到函数超过两个的情况:

#include <iostream>

template<int> struct tag {};
int fun(tag<0>) { return 0; }
int fun(tag<1>) { return 1; }
int fun(tag<2>) { return 2; }

int fun(bool b) {
    if(b) { return fun(tag<0>()); }
    else { return fun(tag<(sizeof(int) == 4) ? 1 : 2>());
}

int main() {
    std::cout << fun(false) << std::endl;
}

您可以将这些函数放在匿名命名空间中,然后使用它们。


当然,还要注意在C ++ 11之前我们有权编写 enable_if 和所有其他的东西 type_traits 为了我们自己
举个例子:

template<bool b, typename = void>
struct enable_if { };

template<typename T>
struct enable_if<true, T> { typedef T type; };

7
2017-09-27 14:36



对于给定的示例来说,这是真的有点过分了,但这也仅仅是为了示例:) - xinaiz
@BlackMoses好吧,如果你有多个功能可供选择,我会用 enable_if作为返回类型。在预C ++ 11中很容易定义。 - skypjack
@BlackMoses在答案中添加了更多细节。 - skypjack
@skypjack我会补充一点 static_assert 从c ++ 11开始提供 - W.F.
@ W.F。很好,我会把它添加到答案中。谢谢。 - skypjack


启用if非常容易实现。看看这个实现:

template<bool b, typename T = void>
struct enable_if {
    typedef T type;
};

template<typename T>
struct enable_if<false, T> {};

在C ++ 11中,我通常会声明一些别名。既然你已经陷入了预C ++ 11时代,那么你可以这样做:

template<bool b>
struct enable_if_parameter : enable_if<b, int*> {};

然后你可以使用这样的结构:

template<typename T, typename enable_if_parameter<(sizeof(T) >= 0)>::type = 0>
void someFunc() {
    // ...
}

如果你可以允许自己一些C ++ 17,你可以这样做:

template<bool b>
using enable_if_parameter = std::enable_if_t<b, int*>;

然后这样做:

template<typename T, enable_if_parameter<std::is_same_v<T, int>> = 0>

我也很喜欢 void_t idom创建新类型特征:

template<typename T, typename = void>
struct has_callme : std::false_type {};

template<typename T>
struct has_callme<T, void_t<decltype(std::declval<T>().callme())>> : std::true_type {};

3
2017-09-27 15:13





在C ++ 03中,您只需编写 enable_if 你自己。它不需要C ++ 11功能。

您使用不同技术的原因是,前C ++ 11编译器有时会对SFINAE和错误应该有什么有趣的定义。 MSVC是目前主要的编译器,他们(在C ++之前的17时代)由于其“SFINAE decltype”问题而对有效的SFINAE有非常古怪的定义。


在C ++ 11中你应该写 void_t 和 enable_if_t 简化你的SFINAE东西。

你也应该这样写:

namespace details {
  template<template<class...>class Z, class always_void, class...Ts>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

这让你可以轻松地写出特征并询问某些东西是否有效(你可以调用一个方法吗?创建一个可以执行某个操作的别名 decltype 在调用上,然后询问您是否可以将类型应用于别名)。这在C ++ 14和17中仍然需要,但C ++ 20可能会得到 is_detected它具有类似的用途。

所以 can_print 是:

template<class T>using print_result = decltype(
  std::declval<std::ostream&>() << std::declval<T>()
);
template<class T>using can_print = can_apply< print_result, T >;

它要么是真的要么是假的,取决于是否 << 使用它在流上工作。

在C ++ 14中,您可以开始使用hana风格的元编程来创建为您进行类型操作的lambda。在C ++ 17中,他们成为了 constexpr,摆脱了一些问题。


使用OP的宏等技术往往会导致程序错误,无需诊断。这是因为如果模板没有有效的模板参数会导致模板的主体成为有效代码,那么您的程序就会形成错误,无需诊断。

所以这:

template<REQUIRES(sizeof(int)==4)>
int fun() {
  // code that is ill-formed if `int` does not have size 4
}

很可能会编译和运行“做你想做的事”,但它实际上是一个不正确的程序 sizeof(int) 是 8

使用此技术根据类的模板参数禁用类上的方法也是如此。标准对此问题尚不清楚,所以我避免使用它。

REQUIRES 宏试图隐藏它在魔法背后的工作原理,但它很容易跨越界限而产生一个不正确的程序。当魔法的细节导致你的代码不正确时隐藏魔法并不是一个好计划。


标签调度可用于简化复杂的SFINAE问题。它可以用于命令重载或在它们之间进行选择,或者将多个类型的包传递给辅助模板函数。

template<std::size_t N>
struct overload_priority : overload_priority<N-1> {};
template<>
struct overload_priority<0> {};

现在你可以通过 overload_priority<50>{} 到一组功能,和一个功能 最高  overload_priority<?> 在那个插槽中将是首选。

template<class T>struct tag_t{using type=T;};

namespace details {
  inline int fun( tag_t<int[4]> ) { return 0; }
  inline int fun( tag_t<int[8]> ) { return 1; }
}
int fun() { return details::fun( tag_t<int[sizeof(int)]>{} ); }

只是根据大小调度到不同的功能 int

fun 重载被编译和检查,因此您不会遇到隐形格式错误的程序问题。

功能有效的函数  它的模板参数的功能是  在C ++中安全使用。你必须使用不同的策略。使这更容易做的机器使得编写不正确的程序变得更容易。


2
2017-09-27 20:16