Pendekatan berfungsi SFINAE di C ++

40

Saya menggunakan fungsi SFINAE dalam suatu proyek dan saya tidak yakin apakah ada perbedaan antara dua pendekatan berikut (selain gaya):

#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

Output program seperti yang diharapkan:

method 1
method 2
Done...

Saya telah melihat metode 2 lebih sering digunakan dalam stackoverflow, tetapi saya lebih suka metode 1.

Apakah ada keadaan ketika kedua pendekatan ini berbeda?

keith
sumber
Bagaimana Anda menjalankan program ini? Itu tidak bisa dikompilasi untuk saya.
ubah igel
@alter igel akan membutuhkan kompiler C ++ 17. Saya menggunakan MSVC 2019 untuk menguji contoh ini, tetapi saya terutama bekerja dengan Dentang.
keith
Terkait: mengapa-harus-saya-hindari-stdenable-jika-dalam-fungsi-tanda tangan dan C ++ 20 juga memperkenalkan cara-cara baru dengan konsep :-)
Jarod42
@ Jarod42 Konsep adalah salah satu hal yang paling saya butuhkan dari C ++ 20.
val berkata Reinstate Monica

Jawaban:

35

Saya telah melihat metode 2 lebih sering digunakan dalam stackoverflow, tetapi saya lebih suka metode 1.

Saran: lebih suka metode 2.

Kedua metode bekerja dengan fungsi tunggal. Masalah muncul ketika Anda memiliki lebih dari satu fungsi, dengan tanda tangan yang sama, dan Anda ingin mengaktifkan hanya satu fungsi dari set.

Misalkan Anda ingin mengaktifkan foo(), versi 1, ketika bar<T>()(berpura-pura itu adalah constexprfungsi) adalah true, dan foo(), versi 2, ketika bar<T>()adalah false.

Dengan

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

Anda mendapatkan kesalahan kompilasi karena Anda memiliki ambiguitas: dua foo()fungsi dengan tanda tangan yang sama (parameter templat default tidak mengubah tanda tangan).

Namun solusi berikut

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

berfungsi, karena SFINAE memodifikasi tanda tangan fungsi.

Pengamatan yang tidak terkait: ada juga metode ketiga: aktifkan / nonaktifkan tipe kembali (kecuali untuk konstruktor kelas / struktur, jelas)

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

Sebagai metode 2, metode 3 kompatibel dengan pemilihan fungsi alternatif dengan tanda tangan yang sama.

maks66
sumber
1
Terima kasih atas penjelasannya, saya akan lebih memilih metode 2 & 3 mulai sekarang :-)
keith
"parameter templat default tidak mengubah tanda tangan" - bagaimana perbedaan ini dalam varian kedua Anda, yang juga menggunakan parameter templat default?
Eric
1
@ Eric - Tidak mudah untuk mengatakan ... Saya kira jawaban yang lain menjelaskan hal ini dengan lebih baik ... Jika SFINAE mengaktifkan / menonaktifkan argumen templat default, foo()fungsi tetap tersedia saat Anda memanggilnya dengan parameter templat kedua eksplisit ( foo<double, double>();panggilan). Dan jika tetap tersedia, ada ambiguitas dengan versi lainnya. Dengan metode 2, SFINAE mengaktifkan / menonaktifkan argumen kedua, bukan parameter default. Jadi Anda tidak dapat menyebutnya explicating the parameter karena ada kegagalan substitusi yang tidak mengizinkan parameter kedua. Jadi versi ini tidak tersedia, jadi tidak ada ambiguitas
max66
3
Metode 3 memiliki keuntungan tambahan karena umumnya tidak bocor ke dalam nama simbol. Varian auto foo() -> std::enable_if_t<...>ini sering berguna untuk menghindari menyembunyikan tanda tangan fungsi dan untuk memungkinkan menggunakan argumen fungsi.
Deduplicator
@ max66: jadi intinya adalah bahwa kegagalan substitusi dalam parameter template default bukan kesalahan jika parameter diberikan dan tidak diperlukan standar?
Eric
21

Selain jawaban max66 , alasan lain untuk memilih metode 2 adalah bahwa dengan metode 1, Anda dapat (secara tidak sengaja) melewati parameter tipe eksplisit sebagai argumen templat kedua dan mengalahkan mekanisme SFINAE sepenuhnya. Ini bisa terjadi sebagai kesalahan ketik, kesalahan salin / tempel, atau sebagai kekeliruan dalam mekanisme templat yang lebih besar.

#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

Demo langsung di sini

ubah igel
sumber
Poin bagus. Mekanismenya bisa dibajak.
maks66