Подтвердить что ты не робот

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

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

Ответ 1

Недавно проголосовали за фундаментальные основы библиотеки 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.

Ответ 2

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

Ответ 3

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

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

Затем мы передаем символ decltype с ошибкой SFIANE на can_apply и выберем тип true/false в зависимости от того, сбой приложения.

Это уменьшает работу на показатель "Я могу сделать X" до минимальной суммы и помещает несколько сложный и хрупкий код SFINAE в изо дня в день.

Я использую С++ 1z void_t. Реализация его сама по себе легко (внизу этого ответа).

Для стандартизации в С++ 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_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 a 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;

который работает в большинстве основных компиляторов С++ 11.

Обратите внимание, что более простой template<class...>using void_t=void; не работает в некоторых старых компиляторах С++ 11 (в стандарте была двусмысленность).

Ответ 4

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

Демо

Ответ 5

Я думаю, что есть две проблемы: 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 и Paul @ 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 s мы никогда не столкнемся с этой ситуацией. Это предотвращает неоднозначность вызовов, даже если несколько алгоритмов жизнеспособны.

Мы определяем наши черты типа:

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

Ответ 6

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