Метапрограммирование: ошибка определения функции определяет отдельную функцию

В этом ответе я определяю шаблон на основе типа 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();
}

Дип предполагает, что вместо 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 ответов

Решение

Недавно проголосовавший за основную библиотеку 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 в ответе Якка.

Используя Уолтера Брауна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 { };

Во-первых, я думаю, что SFINAE обычно следует скрывать от интерфейсов. Это делает интерфейс грязным. Уберите SFINAE от поверхности и используйте диспетчеризацию меток, чтобы определить перегрузку.

Во-вторых, я даже скрываю SFINAE от класса черт. Написание кода "могу я сделать X" достаточно распространено в моем опыте, поэтому я не хочу писать грязный код SFINAE, чтобы сделать это. Поэтому вместо этого я пишу общий can_apply черта, и есть черта, что SFINAE терпит неудачу, если передал неправильные типы, используя decltype,

Затем мы кормим СФИАНУ decltype черта can_applyи получить тип true/false в зависимости от того, произойдет ли сбой приложения.

Это сводит к минимуму объем работы на черту "могу ли я сделать X" и убирает немного хитрый и хрупкий код 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{};
}

Мы можем тогда написать can_apply с точки зрения details::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_string T,

Работа требует, чтобы написать новую черту, как это сейчас 2-4 строки простого кода - просто сделать decltypeusing псевдоним, а затем сделать 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 (в стандарте была неоднозначность).

Вы можете написать вспомогательную черту для этого, используя выражение 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>

демонстрация

Я думаю, что есть две проблемы: 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 Dangerzone и Пола @ 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>, Следовательно, для аргумента типа 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));
    };
}

Макросов можно избежать, если использовать идею Р. Мартиньо Фернандеса:

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

Ну, вы можете просто пропустить всю магию метапрограммирования и использовать 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 тогда функция не может быть вызвана. Это помогает с лучшими сообщениями об ошибках и лучшей компоновке с проверкой требований типа.

Мое мнение: универсально определить, можно ли что-то вызвать, без подробных характеристик типа для каждого и каждого, без использования экспериментальных функций или длинного кода:

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

я нахожу 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.

Другие вопросы по тегам