Bagaimana cara kerja `void_t`

149

Saya menyaksikan pembicaraan Walter Brown di Cppcon14 tentang pemrograman template modern ( Bagian I , Bagian II ) di mana ia mempresentasikan void_tteknik SFINAE- nya .

Contoh:
Diberi templat variabel sederhana yang mengevaluasi voidjika semua argumen templat terbentuk dengan baik:

template< class ... > using void_t = void;

dan sifat berikut yang memeriksa keberadaan variabel anggota yang disebut anggota :

template< class , class = void >
struct has_member : std::false_type
{ };

// specialized as has_member< T , void > or discarded (sfinae)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };

Saya mencoba memahami mengapa dan bagaimana ini bekerja. Karena itu contoh kecil:

class A {
public:
    int member;
};

class B {
};

static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );

1. has_member< A >

  • has_member< A , void_t< decltype( A::member ) > >
    • A::member ada
    • decltype( A::member ) terbentuk dengan baik
    • void_t<> valid dan dievaluasi untuk void
  • has_member< A , void > dan karena itu memilih templat khusus
  • has_member< T , void > dan mengevaluasi ke true_type

2. has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member tidak ada
    • decltype( B::member ) berbentuk buruk dan gagal diam-diam (sfinae)
    • has_member< B , expression-sfinae > jadi templat ini dibuang
  • kompiler ditemukan has_member< B , class = void >dengan void sebagai argumen default
  • has_member< B > mengevaluasi ke false_type

http://ideone.com/HCTlBb

Pertanyaan:
1. Apakah pemahaman saya tentang ini benar?
2. Walter Brown menyatakan bahwa argumen default haruslah tipe yang sama persis seperti yang digunakan void_tagar bisa berfungsi. Mengapa demikian? (Saya tidak mengerti mengapa jenis ini harus cocok, bukankah sembarang jenis standar berfungsi?)

nonsensasi
sumber
6
Iklan 2) Bayangkan menegaskan statis ditulis sebagai: has_member<A,int>::value. Kemudian, spesialisasi parsial yang mengevaluasi has_member<A,void>tidak dapat cocok. Oleh karena itu, perlu has_member<A,void>::value, atau, dengan gula sintaksis, argumen tipe default void.
dyp
1
@d Terima kasih, saya akan mengeditnya. Mh, saya belum melihat ada kebutuhan untuk has_member< T , class = void >defaulting void. Dengan asumsi sifat ini hanya akan digunakan dengan 1 argumen templat kapan saja, maka argumen default dapat berupa jenis apa saja?
nonsensasi
Pertanyaan menarik.
AStopher
2
Perhatikan bahwa Dalam proposal ini, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter berubah template <class, class = void>menjadi template <class, class = void_t<>>. Jadi sekarang kita bebas untuk melakukan apa pun yang kita inginkan dengan void_timplementasi template alias :)
JohnKoch

Jawaban:

133

1. Templat Kelas Utama

Saat Anda menulis has_member<A>::value, kompiler mencari nama has_memberdan menemukan templat kelas utama , yaitu deklarasi ini:

template< class , class = void >
struct has_member;

(Dalam OP, itu ditulis sebagai definisi.)

Daftar argumen templat <A>dibandingkan dengan daftar parameter templat templat primer ini. Sejak template primer memiliki dua parameter, tetapi Anda hanya diberikan satu, parameter yang tersisa gagal untuk template argumen default: void. Seolah-olah Anda telah menulis has_member<A, void>::value.

2. Template Kelas Khusus

Sekarang , daftar parameter template dibandingkan dengan spesialisasi template has_member. Hanya jika tidak ada spesialisasi yang cocok, definisi templat utama digunakan sebagai mundur. Jadi spesialisasi parsial diperhitungkan:

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

Kompiler mencoba untuk mencocokkan argumen templat A, voiddengan pola yang ditentukan dalam spesialisasi parsial: Tdan void_t<..>satu per satu. Pertama , pengurangan argumen templat dilakukan. Spesialisasi parsial di atas masih berupa templat dengan parameter templat yang perlu "diisi" oleh argumen.

Pola pertama T , memungkinkan kompiler untuk menyimpulkan parameter template T. Ini adalah pengurangan sepele, tetapi pertimbangkan pola seperti T const&, di mana kita masih dapat menyimpulkan T. Untuk Targumen pola dan templat A, kami menyimpulkan Tdemikian A.

Dalam pola kedua void_t< decltype( T::member ) > , parameter template Tmuncul dalam konteks di mana parameter tidak dapat dideduksi dari argumen template apa pun.

Ada dua alasan untuk ini:

  • Ekspresi di dalam decltypesecara eksplisit dikecualikan dari pengurangan argumen template. Saya kira ini karena ini bisa sangat rumit.

  • Bahkan jika kita menggunakan pola tanpa decltypesuka void_t< T >, maka pengurangan Tterjadi pada templat alias yang diselesaikan. Yaitu, kami menyelesaikan template alias dan kemudian mencoba menyimpulkan jenis Tdari pola yang dihasilkan. Namun, pola yang dihasilkan adalah void, yang tidak bergantung pada Tdan oleh karena itu tidak memungkinkan kami untuk menemukan tipe spesifik untuk T. Ini mirip dengan masalah matematika dalam mencoba membalikkan fungsi konstan (dalam pengertian matematika dari istilah-istilah itu).

Argumen template deduksi selesai (*) , sekarang yang dideduksi argumen template yang diganti. Ini menciptakan spesialisasi yang terlihat seperti ini:

template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };

Jenis void_t< decltype( A::member ) >sekarang dapat dievaluasi. Ini terbentuk dengan baik setelah substitusi, karenanya, tidak ada Kegagalan Substitusi terjadi. Kita mendapatkan:

template<>
struct has_member<A, void> : true_type
{ };

3. Pilihan

Sekarang , kita dapat membandingkan daftar parameter templat dari spesialisasi ini dengan argumen templat yang disediakan ke aslinya has_member<A>::value. Kedua jenis sama persis, sehingga spesialisasi parsial ini dipilih.


Di sisi lain, ketika kita mendefinisikan templat sebagai:

template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

Kami berakhir dengan spesialisasi yang sama:

template<>
struct has_member<A, void> : true_type
{ };

tetapi daftar argumen templat kami untuk has_member<A>::valuesaat ini adalah <A, int>. Argumen tidak cocok dengan parameter spesialisasi, dan templat utama dipilih sebagai mundur.


(*) Standar, IMHO membingungkan, termasuk proses substitusi dan pencocokan argumen template yang ditentukan secara eksplisit dalam proses pengurangan argumen template . Misalnya (post-N4296) [temp.class.spec.match] / 2:

Spesialisasi parsial cocok dengan daftar argumen templat aktual yang diberikan jika argumen templat dari spesialisasi parsial dapat disimpulkan dari daftar argumen templat aktual.

Tetapi ini tidak hanya berarti bahwa semua parameter templat dari spesialisasi parsial harus disimpulkan; itu juga berarti bahwa substitusi harus berhasil dan (sepertinya?) argumen templat harus cocok dengan parameter templat (diganti) dari spesialisasi parsial. Perhatikan bahwa saya tidak sepenuhnya mengetahui di mana Standar menentukan perbandingan antara daftar argumen yang diganti dan daftar argumen yang disediakan.

dyp
sumber
3
Terima kasih! Saya sudah membacanya berulang-ulang, dan saya kira pemikiran saya tentang bagaimana deduksi argumen template bekerja dengan tepat dan apa yang dipilih oleh kompiler untuk template terakhir saat ini tidak benar.
nonsensation
1
@ JohannesSchaub-litb Terima kasih! Tapi itu agak menyedihkan. Apakah benar-benar tidak ada aturan untuk mencocokkan argumen templat dengan spesialisasi? Bahkan untuk spesialisasi eksplisit?
dyp
2
W / r / t argumen templat default, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC
1
@ Dyp Beberapa minggu kemudian dan membaca banyak tentang ini dan dengan petunjuk dari potongan ini saya pikir saya mulai mengerti bagaimana ini bekerja. Penjelasan Anda dari membaca sampai membaca lebih masuk akal bagi saya, terima kasih!
nonsensasi
1
Saya ingin menambahkan, bahwa istilah templat utama adalah kuncinya (templat pertemuan pertama dalam kode)
nonsensation
18
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

Spesialisasi di atas hanya ada ketika itu terbentuk dengan baik, jadi ketika decltype( T::member )itu valid dan tidak ambigu. spesialisasi itu seperti untuk has_member<T , void>negara dalam komentar.

Ketika Anda menulis has_member<A>, itu has_member<A, void>karena argumen templat default.

Dan kami memiliki spesialisasi untuk has_member<A, void>(jadi mewarisi dari true_type) tetapi kami tidak memiliki spesialisasi untuk has_member<B, void>(jadi kami menggunakan definisi default: mewarisi dari false_type)

Jarod42
sumber