std :: enable_if untuk mengkompilasi fungsi anggota dengan syarat

156

Saya mencoba mendapatkan contoh sederhana untuk bekerja guna memahami cara menggunakan std::enable_if. Setelah saya membaca jawaban ini , saya pikir tidak terlalu sulit untuk memberikan contoh sederhana. Saya ingin menggunakan std::enable_ifuntuk memilih antara dua fungsi anggota dan hanya memperbolehkan salah satunya digunakan.

Sayangnya, yang berikut ini tidak dikompilasi dengan gcc 4.7 dan setelah berjam-jam mencoba saya bertanya kepada kalian apa kesalahan saya.

#include <utility>
#include <iostream>

template< class T >
class Y {

    public:
        template < typename = typename std::enable_if< true >::type >
        T foo() {
            return 10;
        }
        template < typename = typename std::enable_if< false >::type >
        T foo() {
            return 10;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

gcc melaporkan masalah berikut:

% LANG=C make CXXFLAGS="-std=c++0x" enable_if
g++ -std=c++0x    enable_if.cpp   -o enable_if
enable_if.cpp:12:65: error: `type' in `struct std::enable_if<false>' does not name a type
enable_if.cpp:13:15: error: `template<class T> template<class> T Y::foo()' cannot be overloaded
enable_if.cpp:9:15: error: with `template<class T> template<class> T Y::foo()'

Mengapa g ++ tidak menghapus instantiasi yang salah untuk fungsi anggota kedua? Menurut standar, std::enable_if< bool, T = void >::typehanya ada ketika parameter template boolean benar. Tetapi mengapa g ++ tidak menganggap ini sebagai SFINAE? Saya pikir bahwa pesan kesalahan kelebihan beban berasal dari masalah yang g ++ tidak menghapus fungsi anggota kedua dan percaya bahwa ini harus menjadi kelebihan beban.

evnu
sumber
1
Saya tidak yakin, tapi saya pikir ini sebagai berikut: enable_if didasarkan pada SFINAE (kegagalan substitusi bukan kesalahan). Namun, Anda tidak memiliki substitusi di sini, karena tidak ada parameter yang tidak dapat digunakan untuk menentukan kelebihan beban yang digunakan. Anda harus membuat "benar" dan "salah" bergantung pada T. (Saya tahu Anda tidak ingin melakukannya dalam contoh sederhana, tetapi mungkin sekarang terlalu sederhana ...)
Philipp
3
Saya memikirkan itu juga dan mencoba untuk menggunakan std::is_same< T, int >::valuedan ! std::is_same< T, int >::valueyang memberikan hasil yang sama.
evnu

Jawaban:

117

SFINAE hanya berfungsi jika substitusi dalam pengurangan argumen dari argumen templat membuat konstruk menjadi buruk. Tidak ada substitusi seperti itu.

Saya memikirkan itu juga dan mencoba untuk menggunakan std::is_same< T, int >::valuedan ! std::is_same< T, int >::valueyang memberikan hasil yang sama.

Itu karena ketika templat kelas instantiated (yang terjadi ketika Anda membuat objek bertipe di Y<int>antara kasus-kasus lain), templat kelas instantiate semua anggotanya (tidak harus definisi / badannya!). Di antara mereka ada juga templat anggotanya. Perhatikan yang Tdiketahui saat itu, dan !std::is_same< T, int >::valuemenghasilkan false. Jadi itu akan membuat kelas Y<int>yang berisi

class Y<int> {
    public:
        /* instantiated from
        template < typename = typename std::enable_if< 
          std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< true >::type >
        int foo();

        /* instantiated from

        template < typename = typename std::enable_if< 
          ! std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< false >::type >
        int foo();
};

The std::enable_if<false>::typemengakses-non ada jenis, sehingga deklarasi yang sakit-terbentuk. Dan dengan demikian program Anda tidak valid.

Anda perlu membuat templat anggota enable_ifbergantung pada parameter templat anggota itu sendiri. Maka deklarasi tersebut valid, karena seluruh tipe masih tergantung. Saat Anda mencoba memanggil salah satu dari mereka, pengurangan argumen untuk argumen templatnya terjadi dan SFINAE terjadi seperti yang diharapkan. Lihat pertanyaan ini dan jawaban yang sesuai tentang bagaimana melakukan itu.

Johannes Schaub - litb
sumber
14
... Hanya untuk memperjelas, kalau-kalau itu berguna: Ketika turunan dari Ykelas templat adalah instantiated, kompilator tidak akan benar-benar mengkompilasi fungsi anggota templat; Namun, kompiler AKAN melakukan substitusi Tke dalam DECLARATIONS templat anggota sehingga templat anggota ini dapat dipakai di lain waktu. Titik kegagalan ini bukan SFINAE, karena SFINAE hanya berlaku saat menentukan sekumpulan fungsi yang mungkin untuk resolusi kelebihan beban , dan membuat instance kelas bukan merupakan kasus penentuan sekumpulan fungsi untuk resolusi kelebihan muatan. (Atau begitulah menurut saya!)
Dan Nissenbaum
93

Saya membuat contoh singkat ini yang juga berfungsi.

#include <iostream>
#include <type_traits>

class foo;
class bar;

template<class T>
struct is_bar
{
    template<class Q = T>
    typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check()
    {
        return true;
    }

    template<class Q = T>
    typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check()
    {
        return false;
    }
};

int main()
{
    is_bar<foo> foo_is_bar;
    is_bar<bar> bar_is_bar;
    if (!foo_is_bar.check() && bar_is_bar.check())
        std::cout << "It works!" << std::endl;

    return 0;
}

Komentar jika Anda ingin saya menguraikan. Saya pikir kode ini kurang lebih jelas, tetapi sekali lagi saya membuatnya jadi saya mungkin salah :)

Anda dapat melihatnya beraksi di sini .

jpihl
sumber
2
Ini tidak dikompilasi di VS2012. error C4519: default template arguments are only allowed on a class template.
PythonNut
1
Sangat disayangkan. Saya hanya mengujinya dengan gcc. Mungkin ini membantu: stackoverflow.com/a/17543296/660982
jpihl
1
ini tentu jawaban terbaik di sini dan persis apa yang saya cari.
Weipeng L
3
Mengapa ada kebutuhan untuk membuat kelas template lain Q, meskipun sama dengan T?
ilya1725
1
Karena Anda perlu templat testfungsi anggota. Keduanya tidak bisa ada pada saat bersamaan. Qhanya meneruskan tipe templat kelas T. Anda dapat menghapus template kelas Tseperti: cpp.sh/4nxw tapi itu agak mengecewakan tujuannya.
jpihl
13

Bagi mereka yang terlambat datang yang mencari solusi yang "hanya bekerja":

#include <utility>
#include <iostream>

template< typename T >
class Y {

    template< bool cond, typename U >
    using resolvedType  = typename std::enable_if< cond, U >::type; 

    public:
        template< typename U = T > 
        resolvedType< true, U > foo() {
            return 11;
        }
        template< typename U = T >
        resolvedType< false, U > foo() {
            return 12;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

Kompilasi dengan:

g++ -std=gnu++14 test.cpp 

Menjalankan memberi:

./a.out 
11
pengguna1284631
sumber
6
Um, mengapa Anda mengganti nama std::enable_if_tmenjadi resolvedType.
Qwertie
1
Karena tidak semua orang dapat menggunakan C ++ 17 untuk alasan yang dapat sangat bervariasi.
James Yang
9

Dari pos ini :

Argumen templat default bukan bagian dari tanda tangan templat

Tetapi orang dapat melakukan sesuatu seperti ini:

#include <iostream>

struct Foo {
    template < class T,
               class std::enable_if < !std::is_integral<T>::value, int >::type = 0 >
    void f(const T& value)
    {
        std::cout << "Not int" << std::endl;
    }

    template<class T,
             class std::enable_if<std::is_integral<T>::value, int>::type = 0>
    void f(const T& value)
    {
        std::cout << "Int" << std::endl;
    }
};

int main()
{
    Foo foo;
    foo.f(1);
    foo.f(1.1);

    // Output:
    // Int
    // Not int
}
Janek Olszak
sumber
Ini berfungsi, tapi ini pada dasarnya templating fungsi, bukan kelas itu sendiri ... Ini tidak memungkinkan menjatuhkan salah satu dari dua fungsi yang identik-prototyped baik (ketika Anda harus melewati overloading). Namun, idenya bagus. Bisakah Anda menulis ulang contoh OP dalam bentuk kerja?
user1284631
5

Salah satu cara untuk mengatasi masalah ini, spesialisasi fungsi anggota adalah menempatkan spesialisasi ke kelas lain, lalu mewarisi dari kelas itu. Anda mungkin harus mengubah urutan pewarisan untuk mendapatkan akses ke semua data mendasar lainnya tetapi teknik ini berhasil.

template< class T, bool condition> struct FooImpl;
template<class T> struct FooImpl<T, true> {
T foo() { return 10; }
};

template<class T> struct FoolImpl<T,false> {
T foo() { return 5; }
};

template< class T >
class Y : public FooImpl<T, boost::is_integer<T> > // whatever your test is goes here.
{
public:
    typedef FooImpl<T, boost::is_integer<T> > inherited;

    // you will need to use "inherited::" if you want to name any of the 
    // members of those inherited classes.
};

Kerugian dari teknik ini adalah bahwa jika Anda perlu menguji banyak hal yang berbeda untuk fungsi anggota yang berbeda Anda harus membuat kelas untuk masing-masing, dan rantai itu di pohon warisan. Ini berlaku untuk mengakses anggota data umum.

Ex:

template<class T, bool condition> class Goo;
// repeat pattern above.

template<class T, bool condition>
class Foo<T, true> : public Goo<T, boost::test<T> > {
public:
    typedef Goo<T, boost::test<T> > inherited:
    // etc. etc.
};
Gary Powell
sumber
4

Boolean perlu bergantung pada parameter template yang sedang dideduksi. Jadi cara mudah untuk memperbaikinya adalah dengan menggunakan parameter boolean default:

template< class T >
class Y {

    public:
        template < bool EnableBool = true, typename = typename std::enable_if<( std::is_same<T, double>::value && EnableBool )>::type >
        T foo() {
            return 10;
        }

};

Namun, ini tidak akan berfungsi jika Anda ingin membebani fungsi anggota. Sebagai gantinya, yang terbaik untuk digunakan TICK_MEMBER_REQUIRESdari perpustakaan Tick :

template< class T >
class Y {

    public:
        TICK_MEMBER_REQUIRES(std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

        TICK_MEMBER_REQUIRES(!std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

};

Anda juga dapat menerapkan anggota Anda sendiri membutuhkan makro seperti ini (kalau-kalau Anda tidak ingin menggunakan perpustakaan lain):

template<long N>
struct requires_enum
{
    enum class type
    {
        none,
        all       
    };
};


#define MEMBER_REQUIRES(...) \
typename requires_enum<__LINE__>::type PrivateRequiresEnum ## __LINE__ = requires_enum<__LINE__>::type::none, \
class=typename std::enable_if<((PrivateRequiresEnum ## __LINE__ == requires_enum<__LINE__>::type::none) && (__VA_ARGS__))>::type
Paul Fultz II
sumber
Itu tidak berhasil bagi saya seperti itu. Mungkin ada sesuatu yang hilang? Bisakah Anda menulis ulang contoh OP dalam bentuk kerja?
user1284631
Contoh asli tidak berfungsi dengan kelebihan beban. Saya memperbarui jawaban saya bagaimana Anda dapat melakukannya dengan kelebihan beban.
Paul Fultz II
0

Ini adalah contoh minimalis saya, menggunakan makro. Gunakan tanda kurung ganda enable_if((...))saat menggunakan ekspresi yang lebih kompleks.

template<bool b, std::enable_if_t<b, int> = 0>
using helper_enable_if = int;

#define enable_if(value) typename = helper_enable_if<value>

struct Test
{
     template<enable_if(false)>
     void run();
}
Aedoro
sumber