Contoh C ++ SFINAE?

122

Saya ingin masuk ke lebih banyak meta-pemrograman template. Saya tahu bahwa SFINAE adalah singkatan dari "kegagalan substitusi bukanlah kesalahan." Tetapi dapatkah seseorang menunjukkan kepada saya penggunaan yang baik untuk SFINAE?

rlbond.dll
sumber
2
Ini pertanyaan yang bagus. Saya memahami SFINAE dengan cukup baik, tetapi saya rasa saya tidak pernah harus menggunakannya (kecuali perpustakaan melakukannya tanpa saya menyadarinya).
Zifre
5
STL menyatakannya sedikit berbeda dalam FAQ di sini , "Kegagalan Pergantian Bukan Gajah"
gagak vulcan

Jawaban:

72

Berikut salah satu contohnya ( dari sini ):

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

Saat IsClassT<int>::Yesdievaluasi, 0 tidak bisa diubah int int::*karena int bukan kelas, jadi tidak bisa memiliki penunjuk anggota. Jika SFINAE tidak ada, maka Anda akan mendapatkan error compiler, seperti '0 tidak dapat diubah menjadi pointer anggota untuk non-class type int'. Sebaliknya, ini hanya menggunakan ...formulir yang mengembalikan Dua, dan dengan demikian mengevaluasi ke salah, int bukanlah tipe kelas.

Greg Rogers
sumber
8
@rlbond, saya menjawab pertanyaan Anda di komentar untuk pertanyaan ini di sini: stackoverflow.com/questions/822059/… . Singkatnya: Jika kedua fungsi pengujian adalah kandidat dan layak, maka "..." memiliki biaya konversi terburuk, dan karenanya tidak akan pernah digunakan, untuk mendukung fungsi lainnya. "..." adalah elipsis, var-arg thing: int printf (char const *, ...);
Johannes Schaub - litb
Tautan diubah menjadi blog.olivierlanglois.net/index.php/2007/09/01/…
tstenner
20
Hal yang lebih aneh di sini bukanlah IMO ..., melainkan int C::*, yang belum pernah saya lihat dan harus saya cari.
Temukan
1
dapatkah seseorang menjelaskan apa itu C :: *? Saya membaca semua komentar dan tautan, tetapi saya masih bertanya-tanya, int C :: * berarti itu adalah penunjuk anggota tipe int. bagaimana jika kelas tidak memiliki anggota tipe int? Apa yang saya lewatkan? dan bagaimana tes <T> (0) berperan dalam hal ini? Saya pasti melewatkan sesuatu
user2584960
92

Saya suka menggunakan SFINAEuntuk memeriksa kondisi boolean.

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 */
}

Ini bisa sangat berguna. Misalnya, saya menggunakannya untuk memeriksa apakah daftar penginisialisasi yang dikumpulkan menggunakan koma operator tidak lebih dari ukuran tetap

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}

Daftar hanya diterima jika M lebih kecil dari N, yang berarti daftar penginisialisasi tidak terlalu banyak elemen.

Sintaksnya char(*)[C]berarti: Pointer ke array dengan tipe elemen char dan size C. Jika Csalah (0 di sini), maka kita mendapatkan tipe yang tidak valid char(*)[0], penunjuk ke array berukuran nol: SFINAE membuatnya sehingga template akan diabaikan kemudian.

Dinyatakan dengan boost::enable_if, terlihat seperti ini

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, 
           typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}

Dalam prakteknya, saya sering menemukan kemampuan untuk memeriksa kondisi sebagai kemampuan yang berguna.

Johannes Schaub - litb
sumber
1
@Johannes Cukup aneh, GCC (4.8) dan Clang (3.2) menerima untuk mendeklarasikan array berukuran 0 (jadi jenisnya tidak benar-benar "tidak valid"), namun berfungsi dengan baik pada kode Anda. Mungkin ada dukungan khusus untuk kasus ini dalam kasus SFINAE vs. penggunaan tipe "biasa".
akim
@akim: jika itu benar (aneh?! sejak kapan?) maka mungkin M <= N ? 1 : -1bisa berhasil.
v.oddou
1
@ v.oddou Coba saja int foo[0]. Saya tidak heran ini didukung, karena memungkinkan trik yang sangat berguna "struct diakhiri dengan 0-length array" ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
akim
@akim: ya itu yang saya pikirkan -> C99. Ini tidak diperbolehkan di C ++, inilah yang Anda dapatkan dengan kompiler modern:error C2466: cannot allocate an array of constant size 0
v.oddou
1
@ v.oddou Tidak, yang saya maksud adalah C ++, dan sebenarnya C ++ 11: keduanya clang ++ dan g ++ menerimanya, dan saya telah menunjuk ke halaman yang menjelaskan mengapa ini berguna.
akim
16

Dalam C ++ 11, pengujian SFINAE menjadi jauh lebih cantik. Berikut beberapa contoh penggunaan umum:

Pilih fungsi yang kelebihan beban bergantung pada sifatnya

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
}

Menggunakan apa yang disebut idiom tipe sink Anda dapat melakukan tes yang cukup sewenang-wenang pada suatu tipe seperti memeriksa apakah ia memiliki anggota dan jika anggota itu dari tipe tertentu

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

Berikut ini adalah contoh langsungnya: http://ideone.com/dHhyHE Saya juga baru-baru ini menulis seluruh bagian di SFINAE dan pengiriman tag di blog saya (steker tidak tahu malu tetapi relevan) http://metaporky.blogspot.de/2014/08/ bagian-7-static-dispatch-function.html

Perhatikan pada C ++ 14 ada std :: void_t yang pada dasarnya sama dengan TypeSink saya di sini.

odinthenerd
sumber
Blok kode pertama Anda mengubah template yang sama.
TC
Karena tidak ada jenis yang is_integral dan is_floating_point keduanya benar, maka harus salah satu atau karena SFINAE akan menghapus setidaknya satu.
odinthenerd
Anda mendefinisikan ulang template yang sama dengan argumen template default yang berbeda. Sudahkah Anda mencoba menyusunnya?
TC
2
Saya baru mengenal pemrograman metaprogram jadi saya ingin memahami contoh ini. Apakah ada alasan yang Anda gunakan TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>di satu tempat dan kemudian TypeSinkT<decltype(&T::bar)>di tempat lain? Juga apakah &perlu di std::declval<T&>?
Kevin Doyon
1
Tentang Anda TypeSink, C ++ 17 memiliki std::void_t:)
YSC
10

Pustaka enable_if Boost menawarkan antarmuka bersih yang bagus untuk menggunakan SFINAE. Salah satu contoh penggunaan favorit saya ada di pustaka Boost.Iterator . SFINAE digunakan untuk mengaktifkan konversi jenis iterator.

David Joyner
sumber
4

C ++ 17 mungkin akan menyediakan cara umum untuk meminta fitur. Lihat N4502 untuk detailnya, tetapi sebagai contoh mandiri pertimbangkan hal berikut.

Bagian ini adalah bagian yang konstan, taruh di header.

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

Contoh berikut, diambil dari N4502 , menunjukkan penggunaan:

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

Dibandingkan dengan implementasi lainnya, yang satu ini cukup sederhana: seperangkat alat ( void_tdan detect) sudah cukup. Selain itu, telah dilaporkan (lihat N4502 ) bahwa ini jauh lebih efisien (waktu kompilasi dan konsumsi memori kompilator) daripada pendekatan sebelumnya.

Berikut adalah contoh langsung , yang menyertakan penyesuaian portabilitas untuk GCC pra 5.1.

akim
sumber
3

Berikut lain (akhir) SFINAE contoh, berdasarkan Greg Rogers 's jawabannya :

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

Dengan cara ini, Anda dapat memeriksa valuenilai untuk melihat apakah Tkelas atau bukan:

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}
whoan
sumber
Apa arti sintaks ini int C::*dalam jawaban Anda? Bagaimana bisa C::*menjadi nama parameter?
Kirill Kobelev
1
Ini adalah penunjuk ke anggota. Beberapa referensi: isocpp.org/wiki/faq/pointers-to-members
whoan
@KirKobelev int C::*adalah tipe penunjuk ke intvariabel anggota C.
YSC
3

Berikut adalah satu artikel bagus dari SFINAE: Pengantar konsep SFINAE C ++: introspeksi waktu kompilasi anggota kelas .

Rangkumlah sebagai berikut:

/*
 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);
}

declvaladalah utilitas yang memberi Anda "referensi palsu" ke objek dari jenis yang tidak dapat dibuat dengan mudah. declvalsangat berguna untuk konstruksi SFINAE kami.

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
sumber
0

Di sini, saya menggunakan template function overloading (tidak secara langsung SFINAE) untuk menentukan apakah sebuah pointer adalah sebuah fungsi atau pointer kelas anggota: ( Apakah mungkin untuk memperbaiki pointer fungsi anggota iostream cout / cerr yang dicetak sebagai 1 atau benar? )

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

Cetakan

0. false
1. true
2. true
3. true
4. true

Seperti kodenya, ia bisa (tergantung pada kompiler "good" will) menghasilkan panggilan run time ke fungsi yang akan mengembalikan true atau false. Jika Anda ingin memaksa is_function_pointer(var)untuk mengevaluasi pada jenis kompilasi (tidak ada panggilan fungsi yang dilakukan pada waktu proses), Anda dapat menggunakan constexprtrik variabel:

constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;

Dengan standar C ++, semua constexprvariabel dijamin akan dievaluasi pada waktu kompilasi ( Menghitung panjang string C pada waktu kompilasi. Apakah ini benar-benar sebuah konsteks? ).

pengguna
sumber
0

Kode berikut menggunakan SFINAE untuk membiarkan compiler memilih kelebihan beban berdasarkan apakah suatu jenis memiliki metode tertentu atau tidak:

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

Keluaran:

Mengapung: 1
Int: -1
koboi
sumber