Ví dụ về C ++ SFINAE?


122

Tôi muốn tham gia vào lập trình siêu mẫu nhiều hơn. Tôi biết rằng SFINAE là viết tắt của "thay người thất bại không phải là một lỗi." Nhưng ai đó có thể chỉ cho tôi cách sử dụng SFINAE tốt không?


2
Đây là một câu hỏi hay. Tôi hiểu khá rõ về SFINAE, nhưng tôi không nghĩ mình đã từng phải sử dụng nó (trừ khi các thư viện đang làm việc đó mà tôi không biết).
Zifre

5
STL đặt nó hơi khác trong Câu hỏi thường gặp ở đây , "Thay người không thành công không phải là một con voi"
vulcan raven

Câu trả lời:


72

Đây là một ví dụ ( từ đây ):

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

Khi IsClassT<int>::Yesđược đánh giá, 0 không thể được chuyển đổi thành int int::*vì int không phải là một lớp, vì vậy nó không thể có một con trỏ thành viên. Nếu SFINAE không tồn tại, thì bạn sẽ gặp lỗi trình biên dịch, chẳng hạn như '0 không thể chuyển đổi thành con trỏ thành viên cho kiểu int không phải lớp'. Thay vào đó, nó chỉ sử dụng ...biểu mẫu trả về Hai, và do đó đánh giá là false, int không phải là một loại lớp.


8
@rlbond, tôi đã trả lời câu hỏi của bạn trong phần nhận xét cho câu hỏi này tại đây: stackoverflow.com/questions/822059/… . Tóm lại: Nếu cả hai hàm kiểm tra đều là ứng cử viên và khả thi, thì "..." có chi phí chuyển đổi thấp nhất và do đó sẽ không bao giờ được sử dụng, có lợi cho hàm kia. "..." là dấu chấm lửng, thứ var-arg: int printf (char const *, ...);
Johannes Schaub - litb

Liên kết đã thay đổi thành blog.olivierlanglois.net/index.php/2007/09/01/…
tstenner

20
Điều kỳ lạ ở đây IMO không phải là ..., mà là int C::*, thứ mà tôi chưa bao giờ thấy và phải đi tìm. Tìm thấy câu trả lời cho những gì có nghĩa là và những gì nó có thể được sử dụng để ở đây: stackoverflow.com/questions/670734/...
HostileFork nói rằng lòng tin dont SE

1
ai đó có thể giải thích những gì C :: * là? Tôi đã đọc tất cả các bình luận và liên kết, nhưng tôi vẫn đang tự hỏi, int C :: * có nghĩa là nó là một con trỏ thành viên của kiểu int. điều gì sẽ xảy ra nếu một lớp không có thành viên kiểu int? Tôi đang thiếu gì? và kiểm tra <T> (0) đóng vai trò như thế nào? Tôi phải mất một cái gì đó
user2584960

92

Tôi thích sử dụng SFINAEđể kiểm tra các điều kiện 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 */
}

Nó có thể khá hữu ích. Ví dụ: tôi đã sử dụng nó để kiểm tra xem danh sách trình khởi tạo được thu thập bằng cách sử dụng dấu phẩy toán tử không dài hơn kích thước cố định hay không

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

Danh sách chỉ được chấp nhận khi M nhỏ hơn N, nghĩa là danh sách khởi tạo không có quá nhiều phần tử.

Cú pháp char(*)[C]có nghĩa là: Con trỏ đến một mảng có kiểu phần tử là char và kích thước C. Nếu Clà false (0 ở đây), thì chúng tôi nhận được kiểu không hợp lệ char(*)[0], con trỏ đến một mảng có kích thước bằng 0: SFINAE làm cho nó để mẫu sẽ bị bỏ qua sau đó.

Được thể hiện bằng boost::enable_if, trông như thế này

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

Trong thực tế, tôi thường thấy khả năng kiểm tra các điều kiện là một khả năng hữu ích.


1
@Johannes Thật kỳ lạ, GCC (4.8) và Clang (3.2) chấp nhận khai báo mảng có kích thước 0 (vì vậy kiểu không thực sự "không hợp lệ"), nhưng nó hoạt động đúng trên mã của bạn. Có lẽ có sự hỗ trợ đặc biệt cho trường hợp này trong trường hợp SFINAE so với việc sử dụng các loại "thông thường".
akim

@akim: nếu điều đó là đúng (kỳ lạ?! kể từ khi nào?) thì có M <= N ? 1 : -1thể có tác dụng thay thế.
v.oddou

1
@ v.oddou Chỉ cần thử int foo[0]. Tôi không ngạc nhiên khi nó được hỗ trợ, vì nó cho phép thủ thuật "cấu trúc kết thúc bằng mảng độ dài 0" rất hữu ích ( gcc.gnu.org/onlineocs/gcc/Zero-Length.html ).
akim

@akim: đúng như tôi nghĩ -> C99. Này không được phép trong C ++, đây là những gì bạn nhận được với một trình biên dịch hiện đại:error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou Không, tôi thực sự muốn nói là C ++ và thực sự là C ++ 11: cả clang ++ và g ++ đều chấp nhận nó và tôi đã chỉ đến một trang giải thích tại sao điều này lại hữu ích.
akim

16

Trong C ++ 11, các bài kiểm tra SFINAE đã trở nên đẹp hơn nhiều. Dưới đây là một số ví dụ về cách sử dụng phổ biến:

Chọn quá tải chức năng tùy thuộc vào đặc điểm

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
}

Sử dụng một thành ngữ được gọi là loại chìm, bạn có thể thực hiện các bài kiểm tra khá tùy ý trên một loại như kiểm tra xem nó có một thành viên hay không và nếu thành viên đó thuộc một loại nhất định

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

Đây là một ví dụ trực tiếp: http://ideone.com/dHhyHE Gần đây, tôi cũng đã viết toàn bộ một phần về SFINAE và gắn thẻ công văn trong blog của mình (trình cắm xấu hổ nhưng có liên quan) http://metaporky.blogspot.de/2014/08/ part-7-static-dict-function.html

Lưu ý rằng C ++ 14 có std :: void_t về cơ bản giống với TypeSink của tôi ở đây.


Khối mã đầu tiên của bạn xác định lại cùng một mẫu.
TC

Vì không có loại nào mà is_integral và is_floating_point đều đúng nên nó phải là một hoặc vì SFINAE sẽ xóa ít nhất một.
odinthenerd

Bạn đang xác định lại cùng một mẫu với các đối số mẫu mặc định khác nhau. Bạn đã thử biên dịch nó chưa?
TC

2
Tôi mới làm quen với lập trình siêu mẫu nên tôi muốn hiểu ví dụ này. Có lý do gì bạn sử dụng TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>ở nơi này và sau đó TypeSinkT<decltype(&T::bar)>ở nơi khác không? Ngoài ra là &cần thiết trong std::declval<T&>?
Kevin Doyon

1
Về của bạn TypeSink, C ++ 17 có std::void_t:)
YSC

10

Thư viện enable_if của Boost cung cấp giao diện sạch đẹp để sử dụng SFINAE. Một trong những ví dụ sử dụng yêu thích của tôi là trong thư viện Boost.Iterator . SFINAE được sử dụng để bật chuyển đổi loại trình lặp.


4

C ++ 17 có thể sẽ cung cấp một phương tiện chung để truy vấn các tính năng. Xem N4502 để biết chi tiết, nhưng như một ví dụ riêng, hãy xem xét những điều sau.

Phần này là phần không đổi, hãy đặt nó trong tiêu đề.

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

Ví dụ sau, được lấy từ N4502 , cho thấy cách sử dụng:

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

So với các cách triển khai khác, cách triển khai này khá đơn giản: một bộ công cụ được giảm bớt ( void_tdetect) là đủ. Bên cạnh đó, nó đã được báo cáo (xem N4502 ) rằng nó hiệu quả hơn đáng kể (thời gian biên dịch và tiêu thụ bộ nhớ trình biên dịch) so với các cách tiếp cận trước đây.

Đây là một ví dụ trực tiếp , bao gồm các tinh chỉnh về tính di động cho GCC trước 5.1.


3

Đây là một ví dụ SFINAE (muộn) khác , dựa trên câu trả lời của Greg Rogers :

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

Bằng cách này, bạn có thể kiểm tra valuegiá trị của để xem có phải Tlà một lớp hay không:

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}

Cú pháp này int C::*trong câu trả lời của bạn có nghĩa là gì? Làm thế nào có thể C::*là một tên tham số?
Kirill Kobelev

1
Nó là một con trỏ đến thành viên. Một số thông tin tham khảo: isocpp.org/wiki/faq/pointers-to-members
whoan

@KirillKobelev int C::*là loại con trỏ đến intbiến thành viên của C.
YSC

3

Đây là một bài viết hay của SFINAE: Giới thiệu về khái niệm SFINAE của C ++: nội quan thời gian biên dịch của một thành viên trong lớp .

Tóm tắt nó như sau:

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

declvallà một tiện ích cung cấp cho bạn một "tham chiếu giả" đến một đối tượng thuộc loại không thể dễ dàng xây dựng. declvalthực sự hữu ích cho các công trình SFINAE của chúng tôi.

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

0

Ở đây, tôi đang sử dụng tính năng nạp chồng hàm mẫu (không phải trực tiếp SFINAE) để xác định xem con trỏ là một hàm hay con trỏ lớp thành viên: ( Có thể sửa con trỏ hàm thành viên iostream cout / cerr được in là 1 hay đúng không? )

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

Bản in

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

Như mã này, nó có thể (tùy thuộc vào trình biên dịch "good" sẽ) tạo ra một lệnh gọi thời gian chạy tới một hàm sẽ trả về true hoặc false. Nếu bạn muốn buộc is_function_pointer(var)phải đánh giá ở kiểu biên dịch (không có lệnh gọi hàm nào được thực hiện trong thời gian chạy), bạn có thể sử dụng constexprthủ thuật biến:

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

Theo tiêu chuẩn C ++, tất cả các constexprbiến được đảm bảo được đánh giá tại thời điểm biên dịch ( Độ dài tính toán của một chuỗi C tại thời điểm biên dịch. Đây có thực sự là một constexpr? ).


0

Đoạn mã sau sử dụng SFINAE để cho phép trình biên dịch chọn quá tải dựa trên việc một loại có phương thức nhất định hay không:

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

Đầu ra:

Phao: 1
Int: -1
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.