Приклади C ++ SFINAE?


122

Я хочу потрапити на більше мета-програмування шаблонів. Я знаю, що SFINAE розшифровується як «заміна заміни - це не помилка». Але чи може хтось показати мені корисну користь для SFINAE?


2
Це гарне запитання. Я розумію SFINAE досить добре, але не думаю, що мені ніколи не довелося його використовувати (якщо тільки бібліотеки не роблять цього, не знаючи про це).
Зіфре

5
STL сказала дещо інакше у FAQs тут , "Зміна заміни - це не слон"
вулкан ворон

Відповіді:


72

Ось один приклад ( звідси ):

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

20
Більш дивна річ тут ІМО - це не те ..., а, скоріше, те int C::*, чого я ніколи не бачив і не повинен був шукати. Знайдений відповідь на те , що є , і те , що він може бути використаний для тут: stackoverflow.com/questions/670734 / ...
HostileFork говорить не довіряю SE

1
хтось може пояснити, що таке C :: *? Я читаю всі коментарі та посилання, але мені все ще цікаво, int C :: * означає, що це член-покажчик типу int. що робити, якщо у класу немає члена типу int? Що я пропускаю? і як тест <T> (0) грає в це? Я, мабуть, чогось не вистачає
користувач2584960

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]означає: Вказівник на масив з типом елемента та розміром C. Якщо Cзначення false (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) { /* ... */ }
}

На практиці я часто знаходжу здатність перевіряти умови на корисні здібності.


1
@Johannes Як не дивно, GCC (4.8) та Clang (3.2) приймають оголошувати масиви розміром 0 (тому тип насправді не є "недійсним"), але він поводиться належним чином у вашому коді. Напевно, існує спеціальна підтримка у цій справі у випадку SFINAE порівняно з "регулярними" видами використання.
акім

@akim: якщо це колись правда (дивно?! з коли?), то, можливо, M <= N ? 1 : -1міг би працювати замість цього.
v.oddou

1
@ v.oddou Просто спробуйте int foo[0]. Я не здивований, що його підтримують, оскільки це дозволяє дуже корисний трюк "структура, що закінчується масивом 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/ part-7-static-dispatch-function.html

Зауважте, що для C ++ 14 є std :: void_t, який по суті такий же, як і мій TypeSink.


Ваш перший блок коду перевизначає той самий шаблон.
ТК

Оскільки не існує типу, для якого is_integral та is_floating_point є істинними, це має бути або або, оскільки SFINAE видалить принаймні один.
odinthenerd

Ви переосмислюєте той самий шаблон за допомогою різних аргументів шаблону за замовчуванням. Ви спробували її скласти?
ТК

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

Що означає цей синтаксис 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';
}

0

Тут я використовую функцію перевантаження функції шаблону (не безпосередньо SFINAE), щоб визначити, чи вказівник є функцією або вказівником класу члена: ( Чи можливо виправити покажчики функції iostream cout / cerr, що друкуються як 1 чи правда? )

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змінні гарантовано оцінюються під час компіляції ( обчислювальна довжина рядка С у час компіляції. Це справді конспектпр? ).


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
Int: -1
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.