Mengapa saya harus menghindari std :: enable_if dalam tanda tangan fungsi

165

Scott Meyers memposting konten dan status buku berikutnya EC ++ 11. Dia menulis bahwa satu item dalam buku itu bisa menjadi "Hindari std::enable_ifdalam tanda tangan fungsi" .

std::enable_if dapat digunakan sebagai argumen fungsi, sebagai tipe kembali atau sebagai templat kelas atau parameter templat fungsi untuk menghapus fungsi atau kelas secara kondisional dari resolusi kelebihan beban.

Dalam pertanyaan ini ketiga solusi ditampilkan.

Sebagai parameter fungsi:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Sebagai parameter template:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Sebagai jenis pengembalian:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Solusi mana yang lebih disukai dan mengapa saya harus menghindari yang lain?
  • Dalam kasus mana "Hindari std::enable_ifdalam tanda tangan fungsi" menyangkut penggunaan sebagai tipe pengembalian (yang bukan bagian dari tanda tangan fungsi normal tetapi spesialisasi templat)?
  • Apakah ada perbedaan untuk templat fungsi anggota dan non-anggota?
hansmaad
sumber
Karena kelebihan beban sama baiknya, biasanya. Jika ada, delegasikan ke implementasi yang menggunakan templat kelas (khusus).
lihat
Fungsi anggota berbeda karena set overload mencakup kelebihan yang dideklarasikan setelah kelebihan saat ini. Ini sangat penting ketika melakukan variadics tipe pengembalian tertunda (di mana jenis kembali harus disimpulkan dari kelebihan lain)
sehe
1
Nah, hanya subyektif saya harus mengatakan bahwa sementara sering menjadi cukup berguna Saya tidak suka std::enable_ifkekacauan fungsi tanda tangan saya (terutama jelek tambahan nullptrversi fungsi argumen) karena selalu tampak seperti apa itu, hack aneh (untuk sesuatu yang static ifkekuatannya lakukan jauh lebih indah dan bersih) menggunakan template black-magic untuk mengeksploitasi fitur bahasa interresting. Inilah sebabnya mengapa saya lebih suka pengiriman tag bila memungkinkan (well, Anda masih memiliki argumen aneh tambahan, tetapi tidak di antarmuka publik dan juga jauh lebih jelek dan samar ).
Christian Rau
2
Saya ingin bertanya apa yang =0di typename std::enable_if<std::is_same<U, int>::value, int>::type = 0capai? Saya tidak dapat menemukan sumber yang tepat untuk memahaminya. Saya tahu bagian pertama sebelum =0memiliki tipe anggota intjika Udan intsama. Terimakasih banyak!
astroboylrx
4
@astroboylrx Lucu, saya hanya akan memberikan komentar mencatat ini. Pada dasarnya, itu = 0 menunjukkan bahwa ini adalah parameter templat non-tipe yang default . Ini dilakukan dengan cara ini karena parameter templat tipe yang default bukan bagian dari tanda tangan, sehingga Anda tidak dapat membebani mereka secara berlebihan.
Nir Friedman

Jawaban:

107

Letakkan retasan di parameter template .

Pendekatan enable_ifpada template parameter memiliki setidaknya dua keunggulan dibandingkan yang lain:

  • keterbacaan : penggunaan enable_if dan tipe pengembalian / argumen tidak digabung bersama menjadi satu potongan berantakan disambiguator nama ketik dan akses tipe bersarang; meskipun kekacauan disambiguator dan tipe bersarang dapat dikurangi dengan templat alias, itu masih akan menggabungkan dua hal yang tidak terkait bersama. Penggunaan enable_if terkait dengan parameter template, bukan tipe yang dikembalikan. Memiliki mereka di parameter template berarti mereka lebih dekat dengan apa yang penting;

  • penerapan universal : konstruktor tidak memiliki tipe pengembalian, dan beberapa operator tidak dapat memiliki argumen tambahan, sehingga tidak satu pun dari dua opsi lainnya dapat diterapkan di mana-mana. Menempatkan enable_if dalam parameter template berfungsi di mana saja karena Anda hanya dapat menggunakan SFINAE pada templat.

Bagi saya, aspek keterbacaan adalah faktor pendorong besar dalam pilihan ini.

R. Martinho Fernandes
sumber
4
Menggunakan FUNCTION_REQUIRESmakro di sini , membuatnya jauh lebih bagus untuk dibaca, dan bekerja di kompiler C ++ 03 juga, dan itu bergantung pada penggunaan enable_ifdalam tipe kembali. Selain itu, penggunaan enable_ifdalam parameter templat fungsi menyebabkan masalah untuk kelebihan beban, karena sekarang tanda tangan fungsi tidak unik yang menyebabkan kesalahan kelebihan muatan yang ambigu.
Paul Fultz II
3
Ini adalah pertanyaan lama, tetapi bagi siapa pun yang masih membaca: solusi untuk masalah yang diangkat oleh @Paul adalah dengan menggunakan enable_ifparameter templat non-tipe default, yang memungkinkan overloading. Yaitu enable_if_t<condition, int> = 0bukannya typename = enable_if_t<condition>.
Nir Friedman
tautan wayback ke hampir-statis-jika: web.archive.org/web/20150726012736/http
davidbak
@R.MartinhoMenemukan flamingdangerzonetautan dalam komentar Anda tampaknya mengarah ke halaman pemasangan spyware sekarang. Saya menandainya untuk perhatian moderator.
nispio
58

std::enable_ifbergantung pada " Kegagalan Substisi Bukan Kesalahan " (alias SFINAE) selama pengurangan argumen templat . Ini adalah fitur bahasa yang sangat rapuh dan Anda harus sangat berhati-hati untuk memperbaikinya.

  1. jika kondisi Anda di dalam enable_ifberisi templat bersarang atau definisi tipe (petunjuk: cari ::token), maka resolusi tempatles atau tipe bersarang ini biasanya merupakan konteks yang tidak dideduksi . Kegagalan substitusi apa pun pada konteks yang tidak dideduksi adalah kesalahan .
  2. berbagai kondisi secara berganda enable_if kelebihan tidak dapat memiliki tumpang tindih karena resolusi kelebihan akan ambigu. Ini adalah sesuatu yang Anda sebagai penulis perlu periksa sendiri, meskipun Anda akan mendapatkan peringatan kompiler yang baik.
  3. enable_ifmemanipulasi sekumpulan fungsi yang layak selama resolusi kelebihan beban yang dapat memiliki interaksi yang mengejutkan tergantung pada keberadaan fungsi lain yang dibawa dari lingkup lain (misalnya melalui ADL). Ini membuatnya tidak terlalu kuat.

Singkatnya, ketika berhasil, ia bekerja, tetapi jika tidak, bisa sangat sulit untuk di-debug. Alternatif yang sangat baik adalah dengan menggunakan pengiriman tag , yaitu untuk mendelegasikan ke fungsi implementasi (biasanya dalam detailnamespace atau dalam kelas pembantu) yang menerima argumen dummy berdasarkan kondisi waktu kompilasi yang sama yang Anda gunakan di enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

Pengiriman tag tidak memanipulasi set overload, tetapi membantu Anda memilih fungsi yang Anda inginkan dengan memberikan argumen yang tepat melalui ekspresi waktu kompilasi (misalnya dalam sifat tipe). Dalam pengalaman saya, ini jauh lebih mudah untuk di-debug dan diperbaiki. Jika Anda adalah seorang penulis perpustakaan yang bercita-cita untuk ciri-ciri tipe canggih, Anda mungkin perlu enable_ifentah bagaimana, tetapi untuk sebagian besar penggunaan kondisi kompilasi waktu yang teratur itu tidak dianjurkan.

TemplateRex
sumber
22
Namun, pengiriman tag memiliki satu kelemahan: jika Anda memiliki beberapa sifat yang mendeteksi keberadaan suatu fungsi, dan fungsi itu diimplementasikan dengan pendekatan pengiriman tag, ia selalu melaporkan anggota tersebut sebagai hadiah, dan menghasilkan kesalahan alih-alih potensi kegagalan pengganti. . SFINAE terutama merupakan teknik untuk menghilangkan kelebihan dari kandidat set, dan pengiriman tag adalah teknik untuk memilih antara dua (atau lebih) kelebihan. Ada beberapa fungsi yang tumpang tindih, tetapi tidak setara.
R. Martinho Fernandes
@R.MartinhoFernandes dapatkah Anda memberikan contoh singkat, dan menggambarkan bagaimana cara enable_ifmemperbaikinya?
TemplateRex
1
@ R.MartinhoFernandes Saya pikir jawaban terpisah yang menjelaskan poin-poin ini dapat menambah nilai OP. :-) BTW, menulis sifat seperti is_f_ableadalah sesuatu yang saya anggap tugas untuk penulis perpustakaan yang tentu saja dapat menggunakan SFINAE ketika itu memberi mereka keuntungan, tetapi untuk pengguna "biasa" dan diberi sifat is_f_able, saya pikir pengiriman tag lebih mudah.
TemplateRex
1
@hansmaad Saya memposting jawaban singkat untuk menjawab pertanyaan Anda, dan akan membahas masalah "to SFINAE or not not SFINAE" dalam sebuah posting blog sebagai gantinya (ini sedikit di luar topik pada pertanyaan ini). Begitu saya mendapatkan waktu untuk menyelesaikannya, maksud saya.
R. Martinho Fernandes
8
SFINAE "rapuh"? Apa?
Lightness Races dalam Orbit
5

Solusi mana yang lebih disukai dan mengapa saya harus menghindari yang lain?

  • Parameter template

    • Ini dapat digunakan dalam Konstruktor.
    • Ini dapat digunakan di operator konversi yang ditentukan pengguna.
    • Ini membutuhkan C ++ 11 atau lebih baru.
    • Ini adalah IMO, semakin mudah dibaca.
    • Mungkin dengan mudah digunakan salah dan menghasilkan kesalahan dengan kelebihan:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    Perhatikan typename = std::enable_if_t<cond>bukan yang benarstd::enable_if_t<cond, int>::type = 0

  • jenis kembali:

    • Itu tidak dapat digunakan dalam konstruktor. (tidak ada tipe pengembalian)
    • Itu tidak dapat digunakan di operator konversi yang ditentukan pengguna. (tidak dapat dikurangkan)
    • Itu bisa menggunakan pre-C ++ 11.
    • Kedua IMO lebih mudah dibaca.
  • Terakhir, dalam parameter fungsi:

    • Itu bisa menggunakan pre-C ++ 11.
    • Ini dapat digunakan dalam Konstruktor.
    • Itu tidak dapat digunakan di operator konversi yang ditentukan pengguna. (tidak ada parameter)
    • Hal ini tidak dapat digunakan dalam metode dengan jumlah tetap argumen (unary / operator biner +, -,* , ...)
    • Itu dapat dengan aman digunakan dalam warisan (lihat di bawah).
    • Ubah tanda tangan fungsi (pada dasarnya Anda memiliki tambahan sebagai argumen terakhir void* = nullptr) (jadi penunjuk fungsi akan berbeda, dan sebagainya)

Apakah ada perbedaan untuk templat fungsi anggota dan non-anggota?

Ada perbedaan halus dengan warisan dan using :

Menurut using-declarator(penekanan milikku):

namespace.udecl

Himpunan deklarasi yang diperkenalkan oleh using-declarator ditemukan dengan melakukan pencarian nama yang memenuhi syarat ([basic.lookup.qual], [class.member.lookup]) untuk nama dalam using-declarator, tidak termasuk fungsi yang disembunyikan seperti yang dijelaskan di bawah.

...

Ketika menggunakan-deklarator membawa deklarasi dari kelas dasar ke kelas turunan, fungsi anggota dan templat fungsi anggota dalam kelas turunan menimpa dan / atau menyembunyikan fungsi anggota dan templat fungsi anggota dengan nama yang sama, daftar tipe-tipe, daftar cv- kualifikasi, dan ref-kualifikasi (jika ada) di kelas dasar (bukan yang saling bertentangan). Deklarasi tersembunyi atau yang dikesampingkan tersebut dikecualikan dari set deklarasi yang diperkenalkan oleh using-declarator.

Jadi untuk argumen templat dan jenis pengembalian, metode yang disembunyikan adalah skenario berikut:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (gcc salah menemukan fungsi dasar).

Sedangkan dengan argumen, skenario serupa berfungsi:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Demo

Jarod42
sumber