Примеры C ++ SFINAE?

123

Я хочу больше углубиться в метапрограммирование шаблонов. Я знаю, что SFINAE означает «отказ замены не является ошибкой». Но может ли кто-нибудь показать мне хорошее применение SFINAE?

rlbond
источник
2
Это хороший вопрос. Я довольно хорошо понимаю SFINAE, но не думаю, что мне когда-либо приходилось его использовать (если библиотеки не делают это без моего ведома).
Zifre
5
STL сформулировал это несколько иначе в разделе часто задаваемых вопросов : «Ошибка замены - не слон»
vulcan raven

Ответы:

73

Вот один пример ( отсюда ):

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». Вместо этого он просто использует ...форму, которая возвращает Two, и, таким образом, возвращает false, int не является типом класса.

Грег Роджерс
источник
8
@rlbond, я ответил на ваш вопрос в комментариях к этому вопросу здесь: stackoverflow.com/questions/822059/… . Вкратце: если обе тестовые функции являются кандидатами и жизнеспособными, тогда "..." имеет худшую стоимость преобразования и, следовательно, никогда не будет использоваться в пользу другой функции. "..." - это многоточие, аргумент var-arg: int printf (char const *, ...);
Йоханнес Шауб - лит,
Ссылка изменилась на blog.olivierlanglois.net/index.php/2007/09/01/…
tstenner
21
Более странная вещь здесь, ИМО, это не то ..., а скорее то int C::*, чего я никогда не видел, и мне пришлось поискать. Нашел ответ о том, что это такое и для чего его можно использовать здесь: stackoverflow.com/questions/670734/…
HostileFork говорит, что не доверяйте SE
1
может кто-нибудь объяснить, что такое C :: *? Я прочитал все комментарии и ссылки, но мне все еще интересно, int C :: * означает, что это указатель на член типа int. что, если в классе нет члена типа int? Что мне не хватает? и как здесь test <T> (0)? Я, должно быть, чего-то
упускаю
92

Мне нравится использовать 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. Если Cfalse (здесь 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) { /* ... */ }
}

На практике я часто считаю полезной способность проверять условия.

Йоханнес Шауб - litb
источник
1
@Johannes Как ни странно, GCC (4.8) и Clang (3.2) разрешают объявлять массивы размера 0 (так что тип на самом деле не является «недействительным»), но он ведет себя правильно в вашем коде. Вероятно, существует специальная поддержка для этого случая в случае использования типов SFINAE по сравнению с "обычным" использованием типов.
аким
@akim: если это когда-нибудь будет правдой (странно?! с каких пор?), то, может быть, это M <= N ? 1 : -1сработает.
v.oddou
1
@ v.oddou Попробуй int foo[0]. Я не удивлен, что он поддерживается, поскольку он позволяет использовать очень полезный трюк с «структурой, заканчивающейся массивом нулевой длины» ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
аким
@akim: да, я так думал -> C99. Это запрещено в C ++, вот что вы получите с помощью современного компилятора:error C2466: cannot allocate an array of constant size 0
v.oddou
1
@ v.oddou Нет, я действительно имел в виду C ++, а на самом деле C ++ 11: оба clang ++ и g ++ принимают его, и я указал на страницу, объясняющую, почему это полезно.
аким
16

В 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/ часть-7-статическому Ударно-function.html

Обратите внимание, что в C ++ 14 есть std :: void_t, который по сути совпадает с моим TypeSink здесь.

odinthenerd
источник
Ваш первый блок кода переопределяет тот же шаблон.
TC
Поскольку не существует типа, для которого оба is_integral и is_floating_point истинны, он должен быть либо, либо, поскольку SFINAE удалит хотя бы один.
odinthenerd 07
Вы переопределяете один и тот же шаблон с разными аргументами шаблона по умолчанию. Вы пробовали его скомпилировать?
TC
2
Я новичок в метапрограммировании шаблонов, поэтому хотел понять этот пример. Есть ли причина, по которой вы используете TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>в одном месте, а затем TypeSinkT<decltype(&T::bar)>в другом? Также &необходимо в std::declval<T&>?
Кевин Дойон,
1
Насчет вашего TypeSink, C ++ 17 есть std::void_t:)
YSC
10

Библиотека enable_if Boost предлагает приятный чистый интерфейс для использования SFINAE. Один из моих любимых примеров использования - в библиотеке Boost.Iterator . SFINAE используется для включения преобразования типов итератора.

Дэвид Джойнер
источник
4

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 до 5.1.

аким
источник
3

Вот еще один ( в конце) SFINAE пример, основанный на Грег Роджерс «S ответ :

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;
}
whoan
источник
Что означает этот синтаксис int C::*в вашем ответе? Как может C::*быть имя параметра?
Кирилл Кобелев
1
Это указатель на член. Некоторая ссылка: isocpp.org/wiki/faq/pointers-to-members
whoan
@KirillKobelev int C::*- это тип указателя на intпеременную-член C.
YSC
3

Вот одна хорошая статья 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';
}
zangw
источник
0

Здесь я использую перегрузку функции шаблона (не непосредственно SFINAE), чтобы определить, является ли указатель указателем на функцию или класс-член: ( Можно ли исправить указатели функций-членов iostream cout / cerr, которые печатаются как 1 или true? )

https://godbolt.org/z/c2NmzR

#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? ).

пользователь
источник
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
ковбой
источник