C++
SFINAE (ошибка замены не является ошибкой)
Поиск…
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
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 в шаблонах функций
Одной из ограничивающих функций является использование 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
Мотивационный пример
Если в списке параметров шаблона имеется пакет вариативных шаблонов, как в следующем фрагменте кода:
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 следующим образом:
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>{});
}
Существуют и другие методы, которые обычно используются для различения перегрузок, таких как точное совпадение, лучше, чем преобразование, лучше, чем многоточие.
Тем не менее, отправка тегов может распространяться на любое количество вариантов, и это немного более ясно в намерениях.