C++ SFINAE примеры?
Я хочу углубиться в шаблонное метапрограммирование. Я знаю, что SFINAE означает "ошибка замены не является ошибкой". Но может ли кто-нибудь показать мне хорошее применение SFINAE?
11 ответов
Вот один пример ( отсюда):
template<typename T>
class IsClassT {
private:
typedef char One;
typedef struct { char a[2]; } Two;
template<typename C> static One test(int C::*);
// Will be chosen if T is anything except a class.
template<typename C> static Two test(...);
public:
enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };
enum { No = !Yes };
};
когда IsClassT<int>::Yes
оценивается, 0 не может быть преобразовано в int int::*
потому что int не класс, поэтому он не может иметь указатель на член. Если SFINAE не существует, вы получите ошибку компилятора, что-то вроде "0 не может быть преобразовано в указатель на член для не-класса типа int". Вместо этого он просто использует ...
form, которая возвращает Two и, таким образом, оценивается как false, int не является типом класса.
Мне нравится использовать SFINAE
проверить булевы условия.
template<int I> void div(char(*)[I % 2 == 0] = 0) {
/* this is taken when I is even */
}
template<int I> void div(char(*)[I % 2 == 1] = 0) {
/* this is taken when I is odd */
}
Это может быть весьма полезно. Например, я использовал его, чтобы проверить, не больше ли фиксированный размер списка инициализаторов, собранных с помощью запятой оператора.
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}
Список принимается только тогда, когда M меньше N, что означает, что список инициализатора содержит не слишком много элементов.
Синтаксис char(*)[C]
означает: указатель на массив с типом элемента char и размером C
, Если C
ложь (0 здесь), тогда мы получаем неверный тип char(*)[0]
, указатель на массив нулевого размера: SFINAE делает так, что шаблон будет игнорироваться.
Выражается с boost::enable_if
Вот так это выглядит
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i,
typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}
На практике я часто нахожу способность проверять условия полезной способностью.
В C++11 тесты SFINAE стали намного красивее. Вот несколько примеров общего использования:
Выберите перегрузку функции в зависимости от черт
template<typename T>
std::enable_if_t<std::is_integral<T>::value> f(T t){
//integral version
}
template<typename T>
std::enable_if_t<std::is_floating_point<T>::value> f(T t){
//floating point version
}
Используя так называемую идиому приемника типов, вы можете выполнять довольно произвольные тесты для типа, например, проверять, имеет ли он член и имеет ли этот член определенный тип.
//this goes in some header so you can use it everywhere
template<typename T>
struct TypeSink{
using Type = void;
};
template<typename T>
using TypeSinkT = typename TypeSink<T>::Type;
//use case
template<typename T, typename=void>
struct HasBarOfTypeInt : std::false_type{};
template<typename T>
struct HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> :
std::is_same<typename std::decay<decltype(std::declval<T&>().*(&T::bar))>::type,int>{};
struct S{
int bar;
};
struct K{
};
template<typename T, typename = TypeSinkT<decltype(&T::bar)>>
void print(T){
std::cout << "has bar" << std::endl;
}
void print(...){
std::cout << "no bar" << std::endl;
}
int main(){
print(S{});
print(K{});
std::cout << "bar is int: " << HasBarOfTypeInt<S>::value << std::endl;
}
Вот живой пример: http://ideone.com/dHhyHE Я также недавно написал целый раздел о SFINAE и рассылке тегов в своем блоге (бесстыдный плагин, но актуальный) http://metaporky.blogspot.de/2014/08/part-7-static-dispatch-function.html
Обратите внимание, что в C++14 есть std::void_t, который по сути такой же, как мой TypeSink.
Библиотека Boost enable_if предлагает хороший чистый интерфейс для использования SFINAE. Один из моих любимых примеров использования находится в библиотеке Boost.Iterator. SFINAE используется для включения преобразования типов итераторов.
Примеры, приведенные в других ответах, кажутся мне более сложными, чем необходимо.
Вот немного более понятный пример из cppreference :
#include <iostream>
// this overload is always in the set of overloads
// ellipsis parameter has the lowest ranking for overload resolution
void test(...)
{
std::cout << "Catch-all overload called\n";
}
// this overload is added to the set of overloads if
// C is a reference-to-class type and F is a pointer to member function of C
template <class C, class F>
auto test(C c, F f) -> decltype((void)(c.*f)(), void())
{
std::cout << "Reference overload called\n";
}
// this overload is added to the set of overloads if
// C is a pointer-to-class type and F is a pointer to member function of C
template <class C, class F>
auto test(C c, F f) -> decltype((void)((c->*f)()), void())
{
std::cout << "Pointer overload called\n";
}
struct X { void f() {} };
int main(){
X x;
test( x, &X::f);
test(&x, &X::f);
test(42, 1337);
}
Выход:
Reference overload called
Pointer overload called
Catch-all overload called
Как видите, в третьем вызове теста подстановка завершается без ошибок.
Вот еще один (поздний) пример SFINAE, основанный на ответе Greg Rogers:
template<typename T>
class IsClassT {
template<typename C> static bool test(int C::*) {return true;}
template<typename C> static bool test(...) {return false;}
public:
static bool value;
};
template<typename T>
bool IsClassT<T>::value=IsClassT<T>::test<T>(0);
Таким образом, вы можете проверить value
значение, чтобы увидеть, T
это класс или нет:
int main(void) {
std::cout << IsClassT<std::string>::value << std::endl; // true
std::cout << IsClassT<int>::value << std::endl; // false
return 0;
}
В следующем коде используется SFINAE, чтобы компилятор мог выбирать перегрузку в зависимости от того, имеет ли тип определенный метод или нет:
#include <iostream>
template<typename T>
void do_something(const T& value, decltype(value.get_int()) = 0) {
std::cout << "Int: " << value.get_int() << std::endl;
}
template<typename T>
void do_something(const T& value, decltype(value.get_float()) = 0) {
std::cout << "Float: " << value.get_float() << std::endl;
}
struct FloatItem {
float get_float() const {
return 1.0f;
}
};
struct IntItem {
int get_int() const {
return -1;
}
};
struct UniversalItem : public IntItem, public FloatItem {};
int main() {
do_something(FloatItem{});
do_something(IntItem{});
// the following fails because template substitution
// leads to ambiguity
// do_something(UniversalItem{});
return 0;
}
Выход:
Поплавок: 1 Интеллект: -1
C++17, вероятно, предоставит универсальные средства для запроса функций. Подробности смотрите в N4502, но в качестве отдельного примера рассмотрите следующее.
Эта часть является постоянной частью, поместите ее в заголовок.
// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4502.pdf.
template <typename...>
using void_t = void;
// Primary template handles all types not supporting the operation.
template <typename, template <typename> class, typename = void_t<>>
struct detect : std::false_type {};
// Specialization recognizes/validates only types supporting the archetype.
template <typename T, template <typename> class Op>
struct detect<T, Op, void_t<Op<T>>> : std::true_type {};
В следующем примере, взятом из N4502, показано использование:
// Archetypal expression for assignment operation.
template <typename T>
using assign_t = decltype(std::declval<T&>() = std::declval<T const &>())
// Trait corresponding to that archetype.
template <typename T>
using is_assignable = detect<T, assign_t>;
По сравнению с другими реализациями, это довольно просто: сокращенный набор инструментов (void_t
а также detect
) достаточно. Кроме того, сообщалось (см. N4502), что он заметно более эффективен (время компиляции и потребление памяти компилятором), чем предыдущие подходы.
Вот живой пример, который включает в себя настройки переносимости для GCC pre 5.1.
Вот одна хорошая статья SFINAE: Введение в концепцию SFINAE в C++: самоанализ времени компиляции члена класса.
Резюме это следующим образом:
/*
The compiler will try this overload since it's less generic than the variadic.
T will be replace by int which gives us void f(const int& t, int::iterator* b = nullptr);
int doesn't have an iterator sub-type, but the compiler doesn't throw a bunch of errors.
It simply tries the next overload.
*/
template <typename T> void f(const T& t, typename T::iterator* it = nullptr) { }
// The sink-hole.
void f(...) { }
f(1); // Calls void f(...) { }
template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.
template<class T> // A specialisation used if the expression is true.
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.
template <class T> typename enable_if<hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
return obj.serialize();
}
template <class T> typename enable_if<!hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
return to_string(obj);
}
declval
это утилита, которая дает вам "фальшивую ссылку" на объект типа, который не может быть легко создан. declval
действительно удобно для наших конструкций SFINAE.
struct Default {
int foo() const {return 1;}
};
struct NonDefault {
NonDefault(const NonDefault&) {}
int foo() const {return 1;}
};
int main()
{
decltype(Default().foo()) n1 = 1; // int n1
// decltype(NonDefault().foo()) n2 = n1; // error: no default constructor
decltype(std::declval<NonDefault>().foo()) n2 = n1; // int n2
std::cout << "n2 = " << n2 << '\n';
}
Здесь я использую перегрузку функции шаблона (не непосредственно SFINAE), чтобы определить, является ли указатель указателем на функцию или класс-член: ( Можно ли исправить указатели функций-членов iostream cout/cerr, которые печатаются как 1 или true?)
#include<iostream>
template<typename Return, typename... Args>
constexpr bool is_function_pointer(Return(*pointer)(Args...)) {
return true;
}
template<typename Return, typename ClassType, typename... Args>
constexpr bool is_function_pointer(Return(ClassType::*pointer)(Args...)) {
return true;
}
template<typename... Args>
constexpr bool is_function_pointer(Args...) {
return false;
}
struct test_debugger { void var() {} };
void fun_void_void(){};
void fun_void_double(double d){};
double fun_double_double(double d){return d;}
int main(void) {
int* var;
std::cout << std::boolalpha;
std::cout << "0. " << is_function_pointer(var) << std::endl;
std::cout << "1. " << is_function_pointer(fun_void_void) << std::endl;
std::cout << "2. " << is_function_pointer(fun_void_double) << std::endl;
std::cout << "3. " << is_function_pointer(fun_double_double) << std::endl;
std::cout << "4. " << is_function_pointer(&test_debugger::var) << std::endl;
return 0;
}
Печать
0. false
1. true
2. true
3. true
4. true
Как и есть код, он может (в зависимости от "хорошей" воли компилятора) сгенерировать вызов функции во время выполнения, которая вернет истину или ложь. Если вы хотите заставитьis_function_pointer(var)
для оценки типа компиляции (во время выполнения не выполняются вызовы функций) вы можете использовать constexpr
переменный трюк:
constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;
По стандарту C++ все constexpr
переменные гарантированно оцениваются во время компиляции ( вычисление длины строки C во время компиляции. Это действительно constexpr?).
Новый блог существует с момента последнего ответа в этой теме.
Это свободно C++: http://fluentcpp.com/
Есть много примеров для исследования "SFINAE".