如何在 C++11、C++11、14 和 17 之前的版本中简化复杂的 SFINAE 语法?

How to simplify complicated SFINAE syntax, in pre-C++11, C++11, 14 and 17?

这个问题的灵感来自 。我想知道 are/were 在给定标准中简化它的最佳方法是什么。我知道并且个人 used/still 使用,因为 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

并且即使对于非模板函数调用也使用 if:

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

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

原来我用的REQUIRES就是来自这个post.

还有哪些好的技巧?


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

C++11 之前的 SFINAE 示例 (Source):

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

如果您正在使用 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_iftype_traits 中的所有其他内容。
例如:

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

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

Enable 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 惯用语来创造新的类型特征:

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

在 C++03 中,您只需自己编写 enable_if。它不需要 C++11 特性。

您使用不同技术的原因是 C++11 之前的编译器有时对什么是 SFINAE 以及什么应该是错误有一个有趣的定义。 MSVC 是当前的主要编译器,由于 "SFINAE decltype" 问题,它仍然(在 C++17 之前的时代)对什么是有效的 SFINAE 有非常古怪的定义。


在 C++11 中,您应该编写 void_tenable_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
}

很可能会编译 运行 和 "do what you want",但当 sizeof(int)8.

时,它实际上是一个格式错误的程序

使用此技术根据 class 的模板参数禁用 classes 上的方法可能也是如此。标准在这个问题上不清楚,所以我避免了。

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++中使用不是是安全的。你必须使用不同的策略。使这更容易做到的机器只会使编写格式错误的程序变得更容易。