元编程:函数定义失败会定义一个单独的函数。

30
这个答案中,我基于类型的is_arithmetic属性定义了一个模板。
template<typename T> enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
    return to_string(t);
}
template<typename T> enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}

dyp建议,与其使用类型的is_arithmetic属性,不如将是否为该类型定义了to_string作为模板选择标准。这显然是可取的,但我不知道如何表达:

如果未定义std::to_string,则使用ostringstream重载。

声明to_string标准很简单:

template<typename T> decltype(to_string(T{})) stringify(T t){
    return to_string(t);
}

我无法想出与该标准相反的构造方法。显然,这并不起作用,但希望它能传达我试图构建的内容:

template<typename T> enable_if_t<!decltype(to_string(T{})::value, string> (T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}
8个回答

18

使用Walter Brown 的 void_t:

template <typename...>
using void_t = void;

制作这种类型特征非常容易:

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

template<typename T>
struct has_to_string<T, 
    void_t<decltype(std::to_string(std::declval<T>()))>
    > 
: std::true_type { };

2
非常优雅,+1。你知道这个有没有很好的标准化机会吗? - TartanLlama
2
@TartanLlama 没有头绪。至少自己实现非常容易 :) - Barry
4
它已经在下一个标准的草案中了。GCC和Clang在使用"-std=c++1z"编译时拥有它(并且在使用"-std=c++11"时拥有"__void_t"),MSVC 2015也有它。 - Oktalist
2
@JonathanMee 更改什么?我不知道你在说什么。你需要的一切都在Barry的回答中。如果你的标准库实现了它,你可以用std::void_t或者std::__void_t替换void_t,结果是一样的。 - Oktalist
3
使用void_t的好处是它能够正常工作。Matthis Vega的回答无法正常工作 - Barry
显示剩余11条评论

17
首先,我认为SFINAE通常应该隐藏在接口之后。它会使接口变得混乱。将SFINAE放在表面之外,并使用标记分派来选择重载。
其次,我甚至将SFINAE从traits类中隐藏起来。根据我的经验,在写“我是否能做X”代码时很常见,我不想写复杂的SFINAE代码来实现它。因此,我编写了一个通用的can_apply trait,并使用decltype定义了一个在传递了错误类型时SFINAE失败的trait。
然后,我们将SFINAE失败的decltype trait传递给can_apply,并根据应用程序是否失败获取一个true/false类型。
这样可以将每个“我是否能做X” trait的工作量降至最小,并将有些棘手和脆弱的SFINAE代码远离日常工作。
我使用C++1z的void_t。自己实现它很容易(在本答案的底部)。
类似于can_apply的元函数正在C++1z中提出标准化,但它还没有void_t稳定,所以我不使用它。
首先,使用details命名空间将can_apply的实现隐藏起来,以防止意外找到:
namespace details {
  template<template<class...>class Z, class, class...>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
    std::true_type{};
}

我们可以根据 details::can_apply 来编写 can_apply,这样它的接口更加友好(不需要传递额外的 void):
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;

上面是通用的助手元编程代码。一旦我们将其放置,我们可以非常干净地编写一个can_to_string特性类:
template<class T>
using to_string_t = decltype( std::to_string( std::declval<T>() ) );

template<class T>
using can_to_string = can_apply< to_string_t, T >;

我们有一个特性can_to_string<T>,当我们可以对T进行to_string时,它为true。

现在编写类似的新特性只需要2-4行简单的代码 - 只需创建一个decltype using别名,然后对其进行can_apply测试即可。

一旦我们拥有了这个特性,就可以使用标签分派到适当的实现:

template<typename T>
std::string stringify(T t, std::true_type /*can to string*/){
  return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type /*cannot to string*/){
  return static_cast<ostringstream&>(ostringstream() << t).str();
}
template<typename T>
std::string stringify(T t){
  return stringify(t, can_to_string<T>{});
}

所有不好看的代码都藏在details命名空间中。

如果您需要一个void_t,请使用以下内容:

template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;

这段代码在大多数主流的C++11编译器中都能工作。

请注意,更简单的 template<class...>using void_t=void; 在一些旧的C++11编译器中无法正常工作(标准中存在歧义)。


我不喜欢这种标签分发的形式,因为true_typefalse_type的含义仅由调用者定义。仅从分派目标集合中很难立即知道测试的内容。概念仿真的MF(Args)形式具有惰性评估的优点,与别名模板相比。这可以用于防止在直接上下文中不出现的替换失败。 - dyp
@dyp true。这就是为什么我通常会给它一个被注释的名称。在两种情况下都添加了/*can to string*/。我不确定你的MF方法在非立即上下文中避免了什么样的错误,而我的方法没有?是因为choice<>重载或其他原因吗? - Yakk - Adam Nevraumont
不会,而且我不确定你是否会在标签分派中遇到它们(与概念仿真相对)。如果有一个像 is_complete<T>::value && is_trivial<T>::value 这样的复杂表达式,那么你就必须将其拆分,并确保在 is_complete 为 false 时不实例化 is_trivial<T>(否则 UB)。对于更复杂的特性,is_trivial 可以是别名模板,因此您必须延迟实例化。另一方面,is_trivial(T) 不会立即实例化 is_trivial - dyp
1
@Yakk 你在库基础组吗?std::experimental::is_detected 就是你的 can_apply,几乎一样。 - Barry
我认为SFINAE通常应该从接口中隐藏。不同意。类型要求应始终成为接口的一部分。 - Paul Fultz II
显示剩余8条评论

15

最近在上周的委员会会议上,被新鲜投票选入图书馆基础 TS:

template<class T>
using to_string_t = decltype(std::to_string(std::declval<T>()));

template<class T>
using has_to_string = std::experimental::is_detected<to_string_t, T>;

然后对has_to_string进行标签调度和/或SFINAE,尽情发挥。

您可以参考TS的当前工作草案,了解如何实现is_detected和相关函数。这与 @Yakk 的回答中的can_apply非常相似。


@Yakk 在委员会里吗? - Barry
@Barry 你得问他 :) - T.C.
@T.C. 这是否意味着我可以像这样定义我的 ostringstream 重载:template<typename T> enable_if_t<!experimental::is_detected<decltype(std::to_string(std::declval<T>())), T>::value, string> (T t){ return static_cast<ostringstream&>(ostringstream() << t).str(); } - Jonathan Mee
@T.C. 相反,您是在说 decltype 不能作为模板参数传递吗?似乎我以前做过这件事... - Jonathan Mee
1
@JonathanMee 这是一个模板模板参数(提示,重复是有意的),它期望一个类或别名模板作为参数。 - T.C.
显示剩余9条评论

10
您可以使用表达式SFINAE编写一个帮助程序特征来实现此功能:
namespace detail
{
    //base case, to_string is invalid
    template <typename T>
    auto has_to_string_helper (...) //... to disambiguate call
       -> false_type;

    //true case, to_string valid for T
    template <typename T>
    auto has_to_string_helper (int) //int to disambiguate call
       -> decltype(std::to_string(std::declval<T>()), true_type{});
}

//alias to make it nice to use
template <typename T>
using has_to_string = decltype(detail::has_to_string_helper<T>(0));

然后使用 std::enable_if_t<has_to_string<T>::value>

演示


这似乎运作良好,感谢重写。看起来 has_to_string_helper 是一个变量模板或者什么东西,但是 -> 是在做什么?我对箭头运算符的这种用法不熟悉。 - Jonathan Mee
1
has_to_string_helper 是一个模板函数声明。我们从不需要定义它,因为我们只是将其用作编译时结构。-> 使用 C++11 的尾随返回类型语法。我认为在这种情况下更清晰。 - TartanLlama
我难道不需要为每种实际接受 to_string 的类型编写 has_to_string_helper 的重载才能正确返回 true_type 吗?这似乎是很多工作啊! - Jonathan Mee
不,这是一个模板。如果decltype表达式有效,将使用真实的情况,否则将使用假情况。 - TartanLlama
啊,所以 int 是因为你使用函数而不是对象来保存你的类型,所以你需要进行重载。 - Jonathan Mee
“int”在这里是必须的,因为“false_type”重载仍然有效,所以我们需要一些东西来消除调用的歧义。这里有一篇非常好的文章,提供了一种更好的方法来解决这个问题:http://flamingdangerzone.com/cxx11/overload-ranking/。 - TartanLlama

4
我认为有两个问题:1)找到给定类型的所有可行算法。2)选择最佳算法。
例如,我们可以手动为一组重载算法指定顺序:
namespace detail
{
    template<typename T, REQUIRES(helper::has_to_string(T))>
    std::string stringify(choice<0>, T&& t)
    {
        using std::to_string;
        return to_string(std::forward<T>(t));
    }
    
    template<std::size_t N>
    std::string stringify(choice<1>, char const(&arr)[N])
    {
        return std::string(arr, N);
    }
    
    template<typename T, REQUIRES(helper::has_output_operator(T))>
    std::string stringify(choice<2>, T&& t)
    {
        std::ostringstream o;
        o << std::forward<T>(t);
        return std::move(o).str();
    }
}

第一个函数参数指定了这些算法之间的顺序(“首选项”,“次选项”等)。为了选择一个算法,我们只需将其分配给最佳可行匹配:
template<typename T>
auto stringify(T&& t)
    -> decltype( detail::stringify(choice<0>{}, std::forward<T>(t)) )
{
    return detail::stringify(choice<0>{}, std::forward<T>(t));
}

这是如何实现的?我们从Xeo @ Flaming DangerzonePaul @ void_t "can implement concepts"?(使用简化的实现)中借鉴了一些技巧:

constexpr static std::size_t choice_max = 10;
template<std::size_t N> struct choice : choice<N+1>
{
    static_assert(N < choice_max, "");
};
template<> struct choice<choice_max> {};


#include <type_traits>

template<typename T, typename = void> struct models : std::false_type {};
template<typename MF, typename... Args>
struct models<MF(Args...),
                decltype(MF{}.requires_(std::declval<Args>()...),
                         void())>
    : std::true_type {};

#define REQUIRES(...) std::enable_if_t<models<__VA_ARGS__>::value>* = nullptr

选择类继承自更差的选择:choice<0> 继承自 choice<1>。因此,对于类型为 choice<0> 的参数,类型为 choice<0> 的函数参数比 choice<1> 更匹配,而后者比 choice<2> 更匹配,以此类推 [over.ics.rank]p4.4。
请注意,更专业化的决胜方法仅适用于两个函数都不是更好的情况。由于 choice 的总序,我们永远不会遇到这种情况。即使有多个算法可行,这也可以防止调用产生歧义。
我们定义了我们的类型特性:
#include <string>
#include <sstream>
namespace helper
{
    using std::to_string;
    struct has_to_string
    {
        template<typename T>
        auto requires_(T&& t) -> decltype( to_string(std::forward<T>(t)) );
    };
    
    struct has_output_operator
    {
        std::ostream& ostream();
        
        template<typename T>
        auto requires_(T&& t) -> decltype(ostream() << std::forward<T>(t));
    };
}

通过使用R. Martinho Fernandes的一个想法,可以避免使用宏。

template<typename T>
using requires = std::enable_if_t<models<T>::value, int>;

// exemplary application:

template<typename T, requires<helper::has_to_string(T)> = 0>
std::string stringify(choice<0>, T&& t)
{
    using std::to_string;
    return to_string(std::forward<T>(t));
}

@0x499602D2 是的,它会失败。它需要是 static_cast<ostringstream&>(ostringstream{} << x).str():https://dev59.com/6njZa4cB1Zd3GeqPfIh_ - Jonathan Mee
2
@0x499602D2 哦,是的,看起来确实如此:libc++ 定义了以下重载:inline _LIBCPP_INLINE_VISIBILITY typename enable_if<!is_lvalue_reference<_Stream>::value && is_base_of<ios_base, _Stream>::value, _Stream&&>::type operator<<(_Stream&& __os, const _Tp& __x) - dyp
1
@Barry 这是为了防止用户不必提供额外的模板参数。请参见此链接 - TartanLlama
1
@JonathanMee MF 不是一个函数类型。MF(int, double) 是一个函数类型。例如 struct MF {}; MF my_function(int, double); 那么 my_function 的类型是 MF(int, double),而指向 my_function 的指针的类型为 MF(*)(int double)MF{} 创建了一个类型为 MF 的对象,并调用其成员函数 requires_ 来查看是否编译通过。为了将 MF 和参数类型作为单个模板参数传递,我们需要将它们存储在单个类型中。这可以是 tuple<MF, Args...> 或者类似于 MF(Args...) 的函数类型。 - dyp
使用clang编译器,使用std::enable_if宏定义比使用std::enable_if_t更容易获得更好的错误信息。这就是为什么首选std::enable_if宏定义的原因。 - Paul Fultz II
显示剩余8条评论

2

其实,你可以跳过所有的元编程魔法,直接使用fit::conditional适配器,它来自于Fit库:

FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
    [](auto x) -> decltype(to_string(x))
    {
        return to_string(x);
    },
    [](auto x) -> decltype(static_cast<ostringstream&>(ostringstream() << x).str())
    {
        return static_cast<ostringstream&>(ostringstream() << x).str();
    }
);

如果您不介意使用宏,甚至可以更加简洁:

FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
    [](auto x) FIT_RETURNS(to_string(x)),
    [](auto x) FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);

请注意,我还限制了第二个函数,如果该类型不能使用to_string或被流式传输到ostringstream中,那么该函数将无法调用。这有助于更好的错误消息和更好地组合检查类型要求。


有趣,我不熟悉“Fit”。它是什么? - Jonathan Mee
@JonathanMee 这是一个针对C++11/14的函数实用库。我在我的回答中链接了它。 - Paul Fultz II
有趣,它与无处不在的Boost相比如何? - Jonathan Mee
它在Boost库的孵化器中:http://rrsd.com/blincubator.com/alphabetically/ - Paul Fultz II

0
我发现C++20的概念很容易理解。我们可以这样写:
#include<concepts>

template<typename T>
concept has_to_string = requires (T a){ std::to_string(a);};

template<typename T>
auto stringify(T a){
    return "Doesn't have to_string";
}

template<has_to_string T>
auto stringify(T a){
    return "Has to_string";
}

我们可以像这样进行测试:

int main()
{
    int a;
    int b[2];
    std::cout<<stringify(a); // Has to_string
   std::cout<<stringify(b); // Doesn't have to_string
}

编译器 GCC 10.2 标志 -std=c++20


0
我的看法是:为了普遍确定某些东西是否可调用,而不必为每一个东西编写冗长的类型特征或使用实验性功能或长代码。
template<typename Callable, typename... Args, typename = decltype(declval<Callable>()(declval<Args>()...))>
std::true_type isCallableImpl(Callable, Args...) { return {}; }

std::false_type isCallableImpl(...) { return {}; }

template<typename... Args, typename Callable>
constexpr bool isCallable(Callable callable) {
    return decltype(isCallableImpl(callable, declval<Args>()...)){};
}

使用方法:

constexpr auto TO_STRING_TEST = [](auto in) -> decltype(std::to_string(in)) { return {}; };
constexpr bool TO_STRING_WORKS = isCallable<Input>(TO_STRING_TEST);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接