29

这个答案中,我根据类型的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();
}
4

8 回答 8

17

使用沃尔特布朗的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 { };
于 2015-05-12T12:24:04.710 回答
17

首先,我认为 SFINAE 通常应该对接口隐藏。它使界面混乱。将 SFINAE 远离水面,并使用标签调度来挑选过载。

其次,我什至从特征类中隐藏了 SFINAE。根据我的经验,编写“我可以做 X”代码很常见,我不想编写凌乱的 SFINAE 代码来做到这一点。因此,我改为编写一个通用can_apply特征,并且如果使用decltype.

然后,我们将 SFIANE 失败decltype特征提供给can_apply,并根据应用程序是否失败得出真/假类型。

这将每个“我可以做 X”特征的工作量减少到最低限度,并使有些棘手和脆弱的 SFINAE 代码远离日常工作。

我使用 C++1z 的void_t. 自己实现它很容易(在这个答案的底部)。

正在为 C++1z 中的标准化提出一个类似的元函数can_apply,但它不像现在那样稳定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{};
}

然后我们可以用 来写can_applydetails::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>真实的特征,如果我们可以to_stringa T

现在需要编写一个像这样的新特征的工作是 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 编译器中无法工作(标准中存在歧义)。

于 2015-05-12T15:43:58.190 回答
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>;

然后将调度和/或 SFINAE 标记has_to_string到您心中的内容上。

可以参考TS 的当前工作草案如何is_detected和朋友们如何实现。can_apply这与@Yakk 的回答非常相似。

于 2015-05-12T17:24:37.987 回答
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>

演示

于 2015-05-12T12:03:19.173 回答
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“可以实现概念”那里偷一点东西?(使用简化的实现):

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>. 因此,对于 type 的参数,类型choice<0>的函数参数choice<0>choice<1>更好的匹配,比choice<2>等等更好的匹配 [over.ics.rank]p4.4

请注意,仅当两个功能都不是更好时,才适用更专业的平局断路器。由于choices 的总顺序,我们永远不会陷入那种情况。这可以防止调用不明确,即使多种算法是可行的。

我们定义我们的类型特征:

#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));
}
于 2015-05-12T12:56:15.333 回答
2

好吧,您可以跳过所有元编程魔法并使用Fit库中的fit::conditional适配器:

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该函数。这有助于更好的错误消息和更好的可组合性检查类型要求。

于 2015-05-28T19:49:35.390 回答
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);
于 2020-08-28T08:13:42.087 回答
0

我发现conceptsC++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

于 2021-10-26T07:12:10.760 回答