ตัวอย่าง C ++ SFINAE?


122

ฉันต้องการเข้าสู่การเขียนโปรแกรมเมตาเทมเพลตเพิ่มเติม ฉันรู้ว่า SFINAE ย่อมาจาก "ความล้มเหลวในการทดแทนไม่ใช่ข้อผิดพลาด" แต่ใครช่วยบอกวิธีใช้ SFINAE ให้ฉันดูหน่อยได้ไหม


2
นี่เป็นคำถามที่ดี ฉันเข้าใจ SFINAE ดี แต่ฉันไม่คิดว่าจะต้องใช้มันเลย (เว้นแต่ห้องสมุดจะทำโดยที่ฉันไม่รู้)
Zifre

5
STL ให้ความแตกต่างเล็กน้อยในคำถามที่พบบ่อยที่นี่ "การทดแทนความล้มเหลวไม่ใช่ช้าง"
นกกาวัลแคน

คำตอบ:


72

นี่เป็นตัวอย่างหนึ่ง ( จากที่นี่ ):

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

เมื่อIsClassT<int>::Yesได้รับการประเมินจะไม่สามารถแปลง 0 เป็นint int::*เนื่องจาก int ไม่ใช่คลาสจึงไม่สามารถมีตัวชี้สมาชิกได้ หากไม่มี SFINAE คุณจะได้รับข้อผิดพลาดของคอมไพเลอร์บางอย่างเช่น '0 ไม่สามารถแปลงเป็นตัวชี้สมาชิกสำหรับประเภทที่ไม่ใช่คลาส int' แต่จะใช้...รูปแบบที่ส่งคืน Two ดังนั้นจึงประเมินเป็นเท็จ int ไม่ใช่ประเภทคลาส


8
@rlbond ฉันตอบคำถามของคุณในความคิดเห็นของคำถามนี้ที่นี่: stackoverflow.com/questions/822059/… . กล่าวโดยย่อ: หากฟังก์ชันการทดสอบทั้งสองเป็นตัวเลือกและใช้งานได้ "... " จะมีต้นทุนการแปลงที่แย่ที่สุดและจะไม่ถูกนำไปใช้แทนฟังก์ชันอื่น ๆ "... " คือจุดไข่ปลาสิ่ง var-arg: int printf (ถ่าน const *, ... );
Johannes Schaub - litb

ลิงค์เปลี่ยนเป็นblog.olivierlanglois.net/index.php/2007/09/01/…
tstenner

20
สิ่งที่แปลกกว่าที่นี่ IMO ไม่ใช่...แต่เป็นสิ่งint C::*ที่ฉันไม่เคยเห็นและต้องไปดู พบคำตอบว่ามันคืออะไรและใช้ทำอะไรได้ที่นี่: stackoverflow.com/questions/670734/…
HostileFork บอกว่าอย่าไว้วางใจ SE

1
ใครช่วยอธิบายว่า C :: * คืออะไร? ฉันอ่านความคิดเห็นและลิงค์ทั้งหมด แต่ฉันยังสงสัยว่า int C :: * หมายความว่าเป็นตัวชี้สมาชิกประเภท int จะเกิดอะไรขึ้นถ้าชั้นเรียนไม่มีสมาชิกประเภท int? ฉันขาดอะไรไป? และการทดสอบ <T> (0) มีบทบาทอย่างไร ฉันต้องขาดอะไรไป
user2584960

92

ฉันชอบใช้SFINAEเพื่อตรวจสอบเงื่อนไขบูลีน

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

จะมีประโยชน์มากทีเดียว ตัวอย่างเช่นฉันใช้เพื่อตรวจสอบว่ารายการเริ่มต้นที่รวบรวมโดยใช้เครื่องหมายจุลภาคของตัวดำเนินการมีขนาดไม่เกินขนาดคงที่หรือไม่

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

รายการจะได้รับการยอมรับก็ต่อเมื่อ M มีขนาดเล็กกว่า N ซึ่งหมายความว่ารายการตัวเริ่มต้นมีองค์ประกอบไม่มากเกินไป

ไวยากรณ์char(*)[C]หมายถึง: Cตัวชี้ไปยังอาร์เรย์ที่มีประเภทองค์ประกอบถ่านและขนาด ถ้าCเป็นเท็จ (0 ที่นี่) แสดงว่าเราได้ประเภทที่ไม่ถูกต้องchar(*)[0]ชี้ไปที่อาร์เรย์ขนาดศูนย์: SFINAE ทำให้เทมเพลตนั้นถูกละเว้นจากนั้น

แสดงboost::enable_ifว่ามีลักษณะเช่นนี้

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

ในทางปฏิบัติฉันมักพบว่าความสามารถในการตรวจสอบเงื่อนไขเป็นความสามารถที่มีประโยชน์


1
@Johannes น่าแปลกที่ GCC (4.8) และเสียงดัง (3.2) ยอมรับที่จะประกาศอาร์เรย์ขนาด 0 (ดังนั้นประเภทจึงไม่ "ไม่ถูกต้อง" จริงๆ) แต่ก็ทำงานได้อย่างถูกต้องกับรหัสของคุณ อาจมีการสนับสนุนพิเศษสำหรับกรณีนี้ในกรณีของ SFINAE เทียบกับการใช้ประเภท "ปกติ"
akim

@akim: ถ้าเป็นเช่นนั้นจริง (แปลก?! ตั้งแต่เมื่อไหร่?) ก็อาจM <= N ? 1 : -1จะทำงานแทนได้
v.oddou

1
@ v.oddou int foo[0]เพียงแค่พยายาม ฉันไม่แปลกใจเลยว่ามันได้รับการสนับสนุนเนื่องจากช่วยให้เคล็ดลับ "struct ที่ลงท้ายด้วยอาร์เรย์ 0 ความยาว" มีประโยชน์มาก ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html )
akim

@akim: ใช่สิ่งที่ฉันคิด -> C99 สิ่งนี้ไม่ได้รับอนุญาตใน C ++ นี่คือสิ่งที่คุณจะได้รับจากคอมไพเลอร์สมัยใหม่:error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou ไม่ฉันหมายถึง C ++ จริงๆและจริงๆแล้ว C ++ 11: ทั้ง clang ++ และ g ++ ยอมรับและฉันได้ชี้ไปที่หน้าที่อธิบายว่าเหตุใดจึงมีประโยชน์
akim

16

ในการทดสอบ C ++ 11 SFINAE นั้นสวยกว่ามาก ตัวอย่างการใช้งานทั่วไปมีดังนี้

เลือกฟังก์ชันที่มากเกินไปขึ้นอยู่กับลักษณะ

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
}

การใช้สำนวนประเภท sink ที่เรียกว่าคุณสามารถทำการทดสอบโดยพลการในประเภทต่างๆเช่นการตรวจสอบว่ามีสมาชิกหรือไม่และสมาชิกนั้นเป็นประเภทใดประเภทหนึ่งหรือไม่

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

นี่คือตัวอย่างสด: http://ideone.com/dHhyHE ฉันเพิ่งเขียนหัวข้อทั้งหมดใน SFINAE และการจัดส่งแท็กในบล็อกของฉัน (ปลั๊กไร้ยางอาย แต่เกี่ยวข้อง) http://metaporky.blogspot.de/2014/08/ ส่วน-7-คงส่ง-function.html

หมายเหตุสำหรับ C ++ 14 มี std :: void_t ซึ่งโดยพื้นฐานแล้วเหมือนกับ TypeSink ของฉันที่นี่


โค้ดบล็อกแรกของคุณกำหนดเทมเพลตเดียวกันใหม่
TC

เนื่องจากไม่มีประเภทที่ is_integral และ is_floating_point เป็นจริงทั้งคู่จึงควรเป็นอย่างใดอย่างหนึ่งหรือเนื่องจาก SFINAE จะลบอย่างน้อยหนึ่งรายการ
odinthenerd

คุณกำลังกำหนดแม่แบบเดียวกันใหม่โดยมีอาร์กิวเมนต์เทมเพลตเริ่มต้นที่แตกต่างกัน คุณลองรวบรวมแล้วหรือยัง?
TC

2
ฉันเพิ่งเคยใช้เทมเพลต metaprogramming ดังนั้นฉันจึงต้องการทำความเข้าใจกับตัวอย่างนี้ มีเหตุผลที่คุณใช้TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>ในที่หนึ่งและTypeSinkT<decltype(&T::bar)>อีกที่หนึ่งหรือไม่? ยังเป็น&สิ่งที่จำเป็นในstd::declval<T&>?
Kevin Doyon

1
เกี่ยวกับคุณTypeSinkC ++ 17 มีstd::void_t:)
YSC

10

ไลบรารีenable_ifของ Boost มีอินเทอร์เฟซที่สะอาดตาสำหรับการใช้ SFINAE หนึ่งในตัวอย่างการใช้งานที่ฉันชอบอยู่ในไลบรารีBoost.Iterator SFINAE ใช้เพื่อเปิดใช้งานการแปลงประเภท iterator


4

C ++ 17 อาจให้วิธีการทั่วไปในการค้นหาคุณลักษณะต่างๆ ดูN4502รายละเอียดแต่เป็นตัวอย่างในตัวให้พิจารณาสิ่งต่อไปนี้

ส่วนนี้เป็นส่วนคงที่ใส่ไว้ในส่วนหัว

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

ตัวอย่างต่อไปนี้นำมาจากN4502แสดงการใช้งาน:

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

เมื่อเทียบกับการใช้งานอื่น ๆ สิ่งนี้ค่อนข้างง่าย: ชุดเครื่องมือที่ลดลง ( void_tและdetect) พอเพียง นอกจากนี้ยังมีรายงาน (ดูN4502 ) ว่ามีประสิทธิภาพมากกว่า (เวลาคอมไพล์และการใช้หน่วยความจำคอมไพเลอร์) มากกว่าวิธีการก่อนหน้านี้

นี่คือตัวอย่างสดซึ่งรวมถึงการปรับแต่งการพกพาสำหรับ GCC ก่อน 5.1


3

นี่คืออีกตัวอย่างSFINAE (ตอนปลาย) ตามคำตอบของ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);

ด้วยวิธีนี้คุณสามารถตรวจสอบvalueค่าของเพื่อดูว่าTเป็นคลาสหรือไม่:

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

ไวยากรณ์นี้int C::*ในคำตอบของคุณหมายถึงอะไร? วิธีการอาจC::*จะเป็นชื่อพารามิเตอร์หรือไม่?
Kirill Kobelev

1
เป็นตัวชี้ถึงสมาชิก ข้อมูลอ้างอิงบางส่วน: isocpp.org/wiki/faq/pointers-to-members
whoan

@KirillKobelev int C::*เป็นประเภทของตัวชี้ไปนั้นตัวแปรสมาชิกของint C
YSC

3

นี่คือบทความดีๆของ SFINAE: บทนำเกี่ยวกับแนวคิด SFINAE ของ C ++: รวบรวมการวิปัสสนาตามเวลาของสมาชิกในชั้นเรียนเวลารวบรวมวิปัสสนาของสมาชิกชั้นเรียน

สรุปได้ดังต่อไปนี้:

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

declvalเป็นยูทิลิตี้ที่ให้ "การอ้างอิงปลอม" แก่คุณไปยังวัตถุประเภทที่ไม่สามารถสร้างได้ง่ายๆ declvalมีประโยชน์มากสำหรับโครงสร้าง SFINAE ของเรา

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

ที่นี่ฉันกำลังใช้ฟังก์ชัน template มากเกินไป (ไม่ใช่ SFINAE โดยตรง) เพื่อตรวจสอบว่าตัวชี้เป็นฟังก์ชันหรือตัวชี้ระดับสมาชิก: ( เป็นไปได้ไหมที่จะแก้ไขตัวชี้ฟังก์ชันสมาชิก iostream cout / cerr ที่พิมพ์เป็น 1 หรือจริง? )

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

พิมพ์

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

ตามที่เป็นรหัสมันสามารถ (ขึ้นอยู่กับคอมไพเลอร์ "good" will) สร้าง run time call ไปยังฟังก์ชันซึ่งจะคืนค่า true หรือ false หากคุณต้องการบังคับis_function_pointer(var)ให้ทำการประเมินในประเภทคอมไพล์ (ไม่มีการเรียกใช้ฟังก์ชันในขณะรัน) คุณสามารถใช้constexprเคล็ดลับตัวแปร:

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

ตามมาตรฐาน C ++ constexprรับประกันว่าตัวแปรทั้งหมดจะได้รับการประเมินในเวลาคอมไพล์ ( ความยาวการคำนวณของสตริง C ในเวลาคอมไพล์นี่คือ constexpr จริงหรือ? )


0

รหัสต่อไปนี้ใช้ SFINAE เพื่อให้คอมไพลเลอร์เลือกโอเวอร์โหลดโดยขึ้นอยู่กับว่าประเภทมีวิธีการบางอย่างหรือไม่:

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

เอาท์พุท:

ลอย: 1
Int: -1
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.