Metaprograming: Failure of Function Definition Def

2019-01-03 00:09发布

In this answer I define a template based on the type's is_arithmetic property:

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 suggests that rather than the is_arithmetic property of the type, that whether to_string is defined for the type be the template selection criteria. This is clearly desirable, but I don't know a way to say:

If std::to_string is not defined then use the ostringstream overload.

Declaring the to_string criteria is simple:

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

It's the opposite of that criteria that I can't figure out how to construct. This obviously doesn't work, but hopefully it conveys what I'm trying to construct:

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

6条回答
萌系小妹纸
2楼-- · 2019-01-03 00:28

Well, you can just skip all the metaprogramming magic and use the fit::conditional adaptor from the Fit library:

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

Or even more compact, if you don't mind macros:

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

Note, I also constrained the second function as well, so if the type can't be called with to_string nor streamed to ostringstream then the function can't be called. This helps with better error messages and better composability with checking type requirements.

查看更多
手持菜刀,她持情操
3楼-- · 2019-01-03 00:30

First, I think SFINAE should usually be hidden from interfaces. It makes the interface messy. Put the SFINAE away from the surface, and use tag dispatching to pick an overload.

Second, I even hide SFINAE from the traits class. Writing "can I do X" code is common enough in my experience that I don't want to have to write messy SFINAE code to do it. So instead I write a generic can_apply trait, and have a trait that SFINAE fails if passed the wrong types using decltype.

We then feed the SFIANE failing decltype trait to can_apply, and get out a true/false type depending on if the application fails.

This reduces the work per "can I do X" trait to a minimal amount, and places the somewhat tricky and fragile SFINAE code away from day-to-day work.

I use C++1z's void_t. Implementing it yourself is easy (at the bottom of this answer).

A metafunction similar to can_apply is being proposed for standardization in C++1z, but it isn't as stable as void_t is, so I'm not using it.

First, a details namespace to hide the implementation of can_apply from being found by accident:

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

We can then write can_apply in terms of details::can_apply, and it has a nicer interface (it doesn't require the extra void being passed):

template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;

The above is generic helper metaprogramming code. Once we have it in place, we can write a can_to_string traits class very cleanly:

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

and we have a trait can_to_string<T> that is true iff we can to_string a T.

The work require to write a new trait like that is now 2-4 lines of simple code -- just make a decltype using alias, and then do a can_apply test on it.

Once we have that, we use tag dispatching to the proper implementation:

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

All of the ugly code is hiding in the details namespace.

If you need a void_t, use this:

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

which works in most major C++11 compilers.

Note that the simpler template<class...>using void_t=void; fails to work in some older C++11 compilers (there was an ambiguity in the standard).

查看更多
Explosion°爆炸
4楼-- · 2019-01-03 00:34

Freshly voted into the library fundamentals TS at last week's committee meeting:

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

Then tag dispatch and/or SFINAE on has_to_string to your heart's content.

You can consult the current working draft of the TS on how is_detected and friends can be implemented. It's rather similar to can_apply in @Yakk's answer.

查看更多
闹够了就滚
5楼-- · 2019-01-03 00:38

Using Walter Brown's void_t:

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

It's very easy to make such a type trait:

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 { };
查看更多
欢心
6楼-- · 2019-01-03 00:42

You could write a helper trait for this using expression 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));

Then use std::enable_if_t<has_to_string<T>::value>

Demo

查看更多
forever°为你锁心
7楼-- · 2019-01-03 00:47

I think there are two problems: 1) Find all viable algorithms for a given type. 2) Select the best one.

We can, for example, manually specify an order for a set of overloaded algorithms:

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

The first function parameter specifies the order between those algorithms ("first choice", "second choice", ..). In order to select an algorithm, we simply dispatch to the best viable match:

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

How is this implemented? We steal a bit from Xeo @ Flaming Dangerzone and Paul @ void_t "can implement concepts"? (using simplified implementations):

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

The choice classes inherit from worse choices: choice<0> inherits from choice<1>. Therefore, for an argument of type choice<0>, a function parameter of type choice<0> is a better match than choice<1>, which is a better match than choice<2> and so on [over.ics.rank]p4.4

Note that the more specialized tie breaker applies only if neither of two functions is better. Due to the total order of choices, we'll never get into that situation. This prevents calls from being ambiguous, even if multiple algorithms are viable.

We define our type traits:

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

Macros can be avoided by using an idea from 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));
}
查看更多
登录 后发表回答