Поиск…


enable_if

std::enable_if - удобная утилита для использования булевых условий для запуска SFINAE. Он определяется как:

template <bool Cond, typename Result=void>
struct enable_if { };

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

То есть, enable_if<true, R>::type является псевдонимом для R , тогда как enable_if<true, R>::type enable_if<false, T>::type неформатирован, так как эта специализация enable_if не имеет type члена типа.

std::enable_if может использоваться для ограничения шаблонов:

int negate(int i) { return -i; }

template <class F>
auto negate(F f) { return -f(); }

Здесь вызов negate(1) потерпел неудачу из-за двусмысленности. Но вторая перегрузка не предназначена для использования в интегральных типах, поэтому мы можем добавить:

int negate(int i) { return -i; }

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }

Теперь создание экземпляра negate<int> приведет к !std::is_arithmetic<int>::value замены, так как !std::is_arithmetic<int>::value false . Из-за SFINAE это не является жесткой ошибкой, этот кандидат просто удаляется из набора перегрузки. В результате negate(1) имеет только одного жизнеспособного кандидата, который затем называется.

Когда его использовать

Следует иметь в виду, что std::enable_if является помощником поверх SFINAE, но это не то, что делает работу SFINAE в первую очередь. Давайте рассмотрим эти две альтернативы для реализации функциональности, аналогичной std::size , то есть size(arg) набора перегрузки size(arg) который создает размер контейнера или массива:

// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);

// implementation omitted
template<typename Cont>
struct is_sizeable;

// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);

Предполагая, что is_sizeable написано надлежащим образом, эти два объявления должны быть в точности эквивалентными по отношению к SFINAE. Что проще всего написать, и что проще всего просмотреть и понять сразу?

Теперь давайте рассмотрим, как мы можем реализовать арифметические помощники, которые избегают целочисленного переполнения со знаком в сторону обтекания или модульного поведения. То есть, например, incr(i, 3) будет таким же, как i += 3 за исключением того, что результат всегда будет определен, даже если i является int со значением INT_MAX . Это две возможные альтернативы:

// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
 
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
 
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);

Еще раз, что проще всего написать, и который проще всего просматривать и понимать с первого взгляда?

Сила std::enable_if заключается в том, как она работает с рефакторингом и дизайном API. Если is_sizeable<Cont>::value означает, что cont.size() является допустимым, то просто выражение, как оно появляется для size1 может быть более кратким, хотя это может зависеть от того, будет ли is_sizeable использоваться в нескольких местах или нет , Сравните это с std::is_signed который отражает его намерение гораздо более четко, чем когда его реализация просачивается в объявление incr1 .

void_t

C ++ 11

void_t - это мета-функция, которая отображает любое (число) типов для ввода void . Основной целью void_t является упрощение написания признаков типа.

std::void_t будет частью C ++ 17, но до std::void_t пор это было чрезвычайно просто реализовать:

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

Некоторые компиляторы требуют несколько иной реализации:

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type;

Первичным приложением void_t является запись типов типов, которые проверяют правильность утверждения. Например, давайте проверим, имеет ли тип функция-член foo() которая не принимает аргументов:

template <class T, class=void>
struct has_foo : std::false_type {};

template <class T>
struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};

Как это работает? Когда я пытаюсь создать экземпляр has_foo<T>::value , это заставит компилятор попытаться найти лучшую специализацию для has_foo<T, void> . У нас есть два варианта: первичный и вторичный, который включает в себя создание экземпляра этого основного выражения:

  • Если T имеет функцию член foo() , то любой тип , который возвращает конвертируется в void , и специализация предпочтительно первичный на основе шаблона правил частичного упорядочения. Таким образом has_foo<T>::value будет true
  • Если T не имеет такой функции-члена (или для этого требуется более одного аргумента), то подстановка не выполняется для специализации, и у нас есть только основной шаблон для возврата. Следовательно, has_foo<T>::value false .

Простейший случай:

template<class T, class=void>
struct can_reference : std::false_type {};

template<class T>
struct can_reference<T, std::void_t<T&>> : std::true_type {};

это не использует std::declval или decltype .

Вы можете заметить общую схему аргумента void. Мы можем это исключить:

struct details {
  template<template<class...>class Z, class=void, class...Ts>
  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
  {};
};

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

который скрывает использование std::void_t и делает can_apply действующим как индикатор того, является ли тип, предоставленный в качестве первого аргумента шаблона, хорошо сформированным после подстановки в него других типов. Предыдущие примеры теперь можно переписать с помощью can_apply как:

template<class T>
using ref_t = T&;

template<class T>
using can_reference = can_apply<ref_t, T>;    // Is T& well formed for T?

а также:

template<class T>
using dot_foo_r = decltype(std::declval<T&>().foo());

template<class T>
using can_dot_foo = can_apply< dot_foo_r, T >;    // Is T.foo() well formed for T?

который кажется более простым, чем исходные версии.

Есть предложения post-C ++ 17 для std характеристик, похожих на can_apply .

Утилита void_t была обнаружена Уолтером Брауном. Он дал замечательную презентацию на нем в CppCon 2016.

trailing decltype в шаблонах функций

C ++ 11

Одной из ограничивающих функций является использование trailing decltype для указания типа возврата:

namespace details {
   using std::to_string;

   // this one is constrained on being able to call to_string(T)
   template <class T>
   auto convert_to_string(T const& val, int )
       -> decltype(to_string(val))
   {
       return to_string(val);
   }

   // this one is unconstrained, but less preferred due to the ellipsis argument
   template <class T>
   std::string convert_to_string(T const& val, ... )
   {
       std::ostringstream oss;
       oss << val;
       return oss.str();
   }
}

template <class T>
std::string convert_to_string(T const& val)
{
    return details::convert_to_string(val, 0);
}

Если я вызову convert_to_string() с аргументом, с которым я могу вызвать to_string() , то у меня есть две жизнеспособные функции для details::convert_to_string() . Первый предпочтительнее, поскольку преобразование от 0 до int является более лучшей неявной последовательностью преобразования, чем преобразование от 0 до ...

Если я вызываю convert_to_string() с аргументом, из которого я не могу вызывать to_string() , то создание первого экземпляра шаблона функции приводит к decltype(to_string(val)) подстановки (отсутствует decltype(to_string(val)) ). В результате этот кандидат удаляется из набора перегрузки. Второй шаблон функции не ограничен, поэтому он выбирается, и мы вместо этого выполняем operator<<(std::ostream&, T) . Если этот параметр не определен, тогда у нас есть жесткая ошибка компиляции с стеком шаблонов на строке oss << val .

Что такое SFINAE

SFINAE означает S ubstitution F ailure I s N ot A n E rror. Некорректный код, который получается из подстановочных типов (или значений) для создания экземпляра шаблона функции или шаблона класса, не является сложной компиляционной ошибкой, а рассматривается только как отказ от вычета.

Ошибки дедукции при создании экземпляров шаблонов функций или специализации шаблонов классов удаляют этого кандидата из набора соображений - как будто этого неудачного кандидата не существует для начала.

template <class T>
auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }

template <class T, size_t N>
T* begin(T (&arr)[N]) { return arr; }

int vals[10];
begin(vals); // OK. The first function template substitution fails because
             // vals.begin() is ill-formed. This is not an error! That function
             // is just removed from consideration as a viable overload candidate,
             // leaving us with the array overload. 

Только неудачи замещения в непосредственном контексте считаются ошибками дедукции, все остальные считаются трудными ошибками.

template <class T>
void add_one(T& val) { val += 1; }

int i = 4;
add_one(i); // ok

std::string msg = "Hello";
add_one(msg); // error. msg += 1 is ill-formed for std::string, but this
              // failure is NOT in the immediate context of substituting T

enable_if_all / enable_if_any

C ++ 11

Мотивационный пример


Если в списке параметров шаблона имеется пакет вариативных шаблонов, как в следующем фрагменте кода:

template<typename ...Args> void func(Args &&...args) { //... };

Стандартная библиотека (до C ++ 17) не дает прямого способа написать enable_if для ограничения SFINAE для всех параметров в Args или любых параметров в Args . C ++ 17 предлагает std::conjunction и std::disjunction которые решают эту проблему. Например:

/// C++17: SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

/// C++17: SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

Если у вас нет доступных C ++ 17, для их достижения есть несколько решений. Одним из них является использование класса базового класса и частичных специализаций , как показано в ответах на этот вопрос .

В качестве альтернативы, можно также вручную реализовать поведение std::conjunction и std::disjunction в довольно прямом пути. В следующем примере я продемонстрирую реализации и объединим их с std::enable_if для создания двух псевдонимов: enable_if_all и enable_if_any , которые делают именно то, что они предполагают семантически. Это может обеспечить более масштабируемое решение.


Реализация enable_if_all и enable_if_any


Сначала давайте подражать std::conjunction seq_and и std::disjunction используя индивидуальные seq_and и seq_or соответственно:

/// Helper for prior to C++14.
template<bool B, class T, class F >
using conditional_t = typename std::conditional<B,T,F>::type;

/// Emulate C++17 std::conjunction.
template<bool...> struct seq_or: std::false_type {};
template<bool...> struct seq_and: std::true_type {};

template<bool B1, bool... Bs>
struct seq_or<B1,Bs...>: 
  conditional_t<B1,std::true_type,seq_or<Bs...>> {};

template<bool B1, bool... Bs>
struct seq_and<B1,Bs...>:
  conditional_t<B1,seq_and<Bs...>,std::false_type> {};  

Тогда реализация довольно прямолинейна:

template<bool... Bs>
using enable_if_any = std::enable_if<seq_or<Bs...>::value>;

template<bool... Bs>
using enable_if_all = std::enable_if<seq_and<Bs...>::value>;

В конце концов некоторые помощники:

template<bool... Bs>
using enable_if_any_t = typename enable_if_any<Bs...>::type;

template<bool... Bs>
using enable_if_all_t = typename enable_if_all<Bs...>::type;

использование


Использование также прямолинейно:

    /// SFINAE constraints on all of the parameters in Args.
    template<typename ...Args,
             enable_if_all_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

    /// SFINAE constraints on any of the parameters in Args.
    template<typename ...Args,
             enable_if_any_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

is_detected

Чтобы обобщить создание type_trait: на основе SFINAE обнаружены экспериментальные признаки detected_or , detected_t , is_detected .

С параметрами шаблона typename Default , template <typename...> Op и typename ... Args :

  • is_detected : псевдоним std::true_type или std::false_type зависимости от действительности Op<Args...>
  • detected_t : псевдоним Op<Args...> или nonesuch зависимости от действительности Op<Args...> .
  • detected_or : псевдоним структуры с value_t который is_detected , и type который является Op<Args...> или Default зависимости от действительности Op<Args...>

который может быть реализован с использованием std::void_t для SFINAE следующим образом:

C ++ 17
namespace detail {
    template <class Default, class AlwaysVoid,
              template<class...> class Op, class... Args>
    struct detector
    {
        using value_t = std::false_type;
        using type = Default;
    };

    template <class Default, template<class...> class Op, class... Args>
    struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
    {
        using value_t = std::true_type;
        using type = Op<Args...>;
    };

} // namespace detail

// special type to indicate detection failure
struct nonesuch {
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected =
    typename detail::detector<nonesuch, void, Op, Args...>::value_t;

template <template<class...> class Op, class... Args>
using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;

template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;

Затем можно просто реализовать черты для обнаружения присутствия метода:

typename <typename T, typename ...Ts>
using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));

struct C1 {};

struct C2 {
    int foo(char) const;
};

template <typename T>
using has_foo_char = is_detected<foo_type, T, char>;

static_assert(!has_foo_char<C1>::value, "Unexpected");
static_assert(has_foo_char<C2>::value, "Unexpected");

static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value,
              "Unexpected");

static_assert(std::is_same<void, // Default
                           detected_or<void, foo_type, C1, char>>::value,
              "Unexpected");
static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value,
              "Unexpected");

Разрешение перегрузки с большим количеством опций

Если вам нужно выбрать один из нескольких вариантов, включение только одного с помощью enable_if<> может быть довольно громоздким, так как некоторые условия также должны быть сведены на нет.

Порядок между перегрузками может быть выбран с помощью наследования, т.е. отправки меток.

Вместо того, чтобы тестировать вещи, которые должны быть хорошо сформированы, а также проверять отрицание всех других версий, мы вместо этого проверяем только то, что нам нужно, предпочтительно в decltype в трейлинг-возврате.
Это может привести к тому, что несколько вариантов будут хорошо сформированы, мы будем различать те, которые используют «теги», похожие на теги-метки-итераторы ( random_access_tag и др.). Это работает, потому что прямое совпадение лучше, чем базовый класс, который лучше, чем базовый класс базового класса и т. Д.

#include <algorithm>
#include <iterator>

namespace detail
{
    // this gives us infinite types, that inherit from each other
    template<std::size_t N>
    struct pick : pick<N-1> {};
    template<>
    struct pick<0> {};

    // the overload we want to be preferred have a higher N in pick<N>
    // this is the first helper template function
    template<typename T>
    auto stable_sort(T& t, pick<2>)
        -> decltype( t.stable_sort(), void() )
    {
        // if the container have a member stable_sort, use that
        t.stable_sort();
    }

    // this helper will be second best match
    template<typename T>
    auto stable_sort(T& t, pick<1>)
        -> decltype( t.sort(), void() )
    {
        // if the container have a member sort, but no member stable_sort
        // it's customary that the sort member is stable
        t.sort();
    }

    // this helper will be picked last
    template<typename T>
    auto stable_sort(T& t, pick<0>)
        -> decltype( std::stable_sort(std::begin(t), std::end(t)), void() )
    {
        // the container have neither a member sort, nor member stable_sort
        std::stable_sort(std::begin(t), std::end(t));
    }

}

// this is the function the user calls. it will dispatch the call
// to the correct implementation with the help of 'tags'.
template<typename T>
void stable_sort(T& t)
{
    // use an N that is higher that any used above.
    // this will pick the highest overload that is well formed.
    detail::stable_sort(t, detail::pick<10>{});
}

Существуют и другие методы, которые обычно используются для различения перегрузок, таких как точное совпадение, лучше, чем преобразование, лучше, чем многоточие.

Тем не менее, отправка тегов может распространяться на любое количество вариантов, и это немного более ясно в намерениях.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow