ทำไมฉันควรหลีกเลี่ยง std :: enable_if ในฟังก์ชันลายเซ็น


165

Scott Meyers โพสต์เนื้อหาและสถานะของหนังสือเล่มต่อไปของเขา EC ++ 11 เขาเขียนว่าหนึ่งรายการในหนังสือเล่มนี้อาจจะ"หลีกเลี่ยงการstd::enable_ifอยู่ในลายเซ็นฟังก์ชั่น"

std::enable_if สามารถใช้เป็นอาร์กิวเมนต์ฟังก์ชั่นเป็นประเภทผลตอบแทนหรือเป็นแม่แบบชั้นเรียนหรือพารามิเตอร์แม่แบบฟังก์ชั่นเพื่อลบฟังก์ชั่นหรือคลาสจากเงื่อนไขการแก้ไขเกินพิกัด

ในคำถามนี้จะแสดงคำตอบทั้งสาม

พารามิเตอร์ฟังก์ชั่น:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

เป็นพารามิเตอร์เทมเพลต:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

ในฐานะที่เป็นประเภทผลตอบแทน:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • โซลูชันใดควรเป็นที่ต้องการและทำไมฉันจึงควรหลีกเลี่ยงผู้อื่น
  • ในกรณีใด"หลีกเลี่ยงstd::enable_ifในฟังก์ชั่นลายเซ็น"เกี่ยวข้องกับการใช้งานเป็นชนิดส่งคืน (ซึ่งไม่ได้เป็นส่วนหนึ่งของลายเซ็นฟังก์ชั่นปกติ แต่ของความเชี่ยวชาญแม่แบบ)?
  • มีความแตกต่างสำหรับแม่แบบฟังก์ชันสมาชิกและไม่ใช่สมาชิกหรือไม่?

เพราะการบรรทุกเกินพิกัดนั้นดีเหมือนปกติ หากมีอะไรให้มอบสิทธิ์ในการใช้งานที่ใช้เทมเพลตคลาส
sehe

ฟังก์ชั่นสมาชิกแตกต่างกันในที่เกินพิกัดชุดรวมถึงการโอเวอร์โหลดประกาศหลังจากเกินพิกัดปัจจุบัน นี้เป็นสิ่งสำคัญโดยเฉพาะอย่างยิ่งเมื่อ variadics ทำล่าช้าชนิดกลับ (ที่ประเภทกลับที่จะอนุมานจากการโอเวอร์โหลดอื่น)
sehe

1
ดีแค่จิตใจฉันได้กล่าวว่าในขณะที่มักจะเป็นประโยชน์มากฉันไม่ชอบstd::enable_ifที่จะถ่วงลายเซ็นฟังก์ชั่นของฉัน (โดยเฉพาะอย่างยิ่งน่าเกลียดเพิ่มเติมnullptrรุ่นอาร์กิวเมนต์ฟังก์ชั่น) เพราะมันก็ดูเหมือนว่ามันคืออะไรสับแปลก (สำหรับบางสิ่งบางอย่างstatic ifอาจ ทำสิ่งที่สวยงามและสะอาดยิ่งขึ้น) โดยใช้เทมเพลตมนต์ดำเพื่อใช้ประโยชน์จากคุณสมบัติด้านภาษา นี่คือเหตุผลที่ฉันชอบการแจกจ่ายแท็กเมื่อใดก็ตามที่เป็นไปได้ (ดีคุณยังคงมีข้อโต้แย้งแปลก ๆ เพิ่มเติม แต่ไม่ได้อยู่ในส่วนต่อประสานสาธารณะและน่าเกลียดและซ่อนเร้นน้อยกว่า)
Christian Rau

2
ผมอยากจะขอให้สิ่งที่ไม่=0ในการtypename std::enable_if<std::is_same<U, int>::value, int>::type = 0ประสบความสำเร็จ? ฉันไม่พบแหล่งข้อมูลที่ถูกต้องเพื่อทำความเข้าใจ ฉันรู้ว่าส่วนแรกก่อน=0มีประเภทสมาชิกintถ้าUและintเหมือนกัน ขอบคุณมาก!
astroboylrx

4
@astroboylrx ขำ ๆ ฉันเพิ่งจะใส่ความเห็นสังเกตสิ่งนี้ โดยทั่วไปนั้น = 0 บ่งชี้ว่านี่เป็นพารามิเตอร์เทมเพลตที่ไม่ใช่ค่าดีฟอลต์ ใช้วิธีนี้เนื่องจากพารามิเตอร์เทมเพลตชนิดที่เป็นค่าเริ่มต้นไม่ได้เป็นส่วนหนึ่งของลายเซ็นดังนั้นคุณจึงไม่สามารถโหลดเกินขนาด
Nir Friedman

คำตอบ:


107

ใส่สับในพารามิเตอร์แม่แบบ

วิธีการenable_ifตามพารามิเตอร์เทมเพลตมีข้อดีอย่างน้อยสองประการเหนือข้ออื่น

  • ความสามารถในการอ่าน : การใช้ enable_if และชนิด return / อาร์กิวเมนต์จะไม่ถูกรวมเข้าด้วยกันเป็นหนึ่งชิ้นยุ่งเหยิงของ disambiguators ชื่อพิมพ์และการเข้าถึงประเภทซ้อนกัน; แม้ว่าความยุ่งเหยิงของ disambiguator และชนิดซ้อนกันสามารถบรรเทาได้ด้วยเทมเพลต alias ซึ่งจะยังคงผสานสองสิ่งที่ไม่เกี่ยวข้องเข้าด้วยกัน การเปิดใช้งาน enable_if เกี่ยวข้องกับพารามิเตอร์เทมเพลตไม่ใช่ชนิดที่ส่งคืน การมีพารามิเตอร์เทมเพลตหมายความว่าพวกมันใกล้เคียงกับสิ่งที่สำคัญ

  • การบังคับใช้สากล : คอนสตรัคเตอร์ไม่มีประเภทส่งคืนและผู้ประกอบการบางรายไม่สามารถมีข้อโต้แย้งเพิ่มเติมดังนั้นจึงไม่มีตัวเลือกอีกสองตัวเลือกที่สามารถใช้ได้ทุกที่ การเปิดใช้งาน enable_if ในพารามิเตอร์เทมเพลตทำงานได้ทุกที่เนื่องจากคุณสามารถใช้ SFINAE กับเทมเพลตได้เท่านั้น

สำหรับฉันความสามารถในการอ่านเป็นปัจจัยสร้างแรงบันดาลใจที่ยิ่งใหญ่ในตัวเลือกนี้


4
การใช้FUNCTION_REQUIRESมาโครที่นี่ทำให้การอ่านดีขึ้นมากและทำงานในคอมไพเลอร์ C ++ 03 เช่นกันและใช้การใช้งานenable_ifในชนิดส่งคืน นอกจากนี้การใช้enable_ifพารามิเตอร์เทมเพลตของฟังก์ชั่นทำให้เกิดปัญหาในการโอเวอร์โหลดเพราะตอนนี้ลายเซ็นของฟังก์ชั่นนั้นไม่ซ้ำกันทำให้เกิดข้อผิดพลาดการโอเวอร์โหลดที่คลุมเครือ
Paul Fultz II

3
นี่เป็นคำถามเก่า แต่สำหรับทุกคนที่ยังคงอ่านอยู่: การแก้ปัญหาที่ยกขึ้นโดย @Paul คือการใช้enable_ifกับพารามิเตอร์เท็มเพลตที่ไม่ใช่ค่าเริ่มต้นซึ่งอนุญาตให้มีการโหลดมากเกินไป คือแทนenable_if_t<condition, int> = 0 typename = enable_if_t<condition>
Nir Friedman

ลิงก์ย้อนกลับไปยังเกือบคงที่: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…
davidbak

@ R.MartinhoFernandes flamingdangerzoneลิงก์ในความคิดเห็นของคุณดูเหมือนจะนำไปสู่หน้าการติดตั้งสปายแวร์ในขณะนี้ ฉันตั้งค่าสถานะเพื่อความสนใจของผู้ดูแล
nispio

58

std::enable_ifอาศัยหลักการ " ล้มเหลวของ Substition Is Not An Error " (aka SFINAE) ในระหว่างการลดอาร์กิวเมนต์เท็มเพลต นี่เป็นคุณสมบัติภาษาที่บอบบางมากและคุณต้องระวังให้ถูกต้อง

  1. ถ้าสภาพของคุณภายในenable_ifมีแม่แบบซ้อนกันหรือการกำหนดประเภท (คำใบ้: มองหา::ราชสกุล) แล้วมติ tempatles ซ้อนกันเหล่านี้หรือประเภทที่มีมักจะเป็นบริบทที่ไม่อนุมาน ความล้มเหลวใด ๆ เกี่ยวกับการทดแทนเช่นบริบทที่ไม่อนุมานเป็นข้อผิดพลาด
  2. เงื่อนไขต่างๆในการenable_ifโอเวอร์โหลดหลายครั้งไม่สามารถซ้อนทับกันได้เนื่องจากการแก้ปัญหาการโอเวอร์โหลดจะไม่ชัดเจน นี่คือสิ่งที่คุณในฐานะผู้เขียนต้องตรวจสอบตัวเองแม้ว่าคุณจะได้รับคำเตือนผู้รวบรวมที่ดี
  3. enable_ifปรับเปลี่ยนชุดของฟังก์ชั่นที่ทำงานได้ในระหว่างการแก้ปัญหาการโอเวอร์โหลดซึ่งสามารถมีปฏิสัมพันธ์ที่น่าประหลาดใจขึ้นอยู่กับการปรากฏตัวของฟังก์ชั่นอื่น ๆ ที่นำเข้ามาจากขอบเขตอื่น ๆ (เช่นผ่าน ADL) สิ่งนี้ทำให้ไม่แข็งแกร่งมาก

กล่าวโดยย่อเมื่อทำงานได้ผล แต่เมื่อไม่สามารถแก้ไขข้อบกพร่องได้ยาก ทางเลือกที่ดีมากคือการใช้การฝึกอบรมแท็กคือการมอบหมายให้ฟังก์ชั่นการใช้งาน (มักจะอยู่ในdetailnamespace หรือในระดับผู้ช่วย) enable_ifที่ได้รับการโต้แย้งหุ่นซึ่งเป็นไปตามเงื่อนไขที่รวบรวมเวลาเดียวกันกับที่คุณใช้ใน

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

การจัดส่งแท็กไม่ได้จัดการชุดโอเวอร์โหลด แต่ช่วยให้คุณสามารถเลือกฟังก์ชั่นที่คุณต้องการได้อย่างแท้จริงด้วยการระบุอาร์กิวเมนต์ที่เหมาะสมผ่านการแสดงออกของเวลาคอมไพล์ (เช่นในลักษณะประเภท) จากประสบการณ์ของฉันการดีบักและทำให้ถูกต้องง่ายขึ้น หากคุณเป็นนักเขียนห้องสมุดที่ต้องการคุณสมบัติประเภทที่ซับซ้อนคุณอาจต้องการenable_ifบางอย่าง แต่สำหรับการใช้งานทั่วไปของเงื่อนไขการคอมไพล์เวลาก็ไม่แนะนำให้ใช้


22
การจัดส่งแท็กมีข้อเสียอย่างหนึ่งแม้ว่า: หากคุณมีคุณลักษณะบางอย่างที่ตรวจจับการมีอยู่ของฟังก์ชันและฟังก์ชันนั้นถูกนำไปใช้ด้วยวิธีการจัดส่งแท็กมันจะรายงานว่าสมาชิกนั้นเป็นปัจจุบันเสมอและส่งผลให้เกิดข้อผิดพลาดแทน . SFINAE เป็นเทคนิคหลักในการลบโอเวอร์โหลดออกจากชุดผู้สมัครและแท็กการจัดส่งเป็นเทคนิคสำหรับการเลือกระหว่างโอเวอร์โหลดสองอัน (หรือมากกว่า) มีฟังก์ชั่นทับซ้อนบ้าง แต่ก็ไม่เทียบเท่ากัน
R. Martinho Fernandes

@ R.MartinhoFernandes คุณสามารถยกตัวอย่างสั้น ๆ และแสดงให้เห็นว่าenable_ifจะทำให้ถูกต้องได้อย่างไร
TemplateRex

1
@ R.MartinhoFernandes ฉันคิดว่าคำตอบที่แยกต่างหากเพื่ออธิบายประเด็นเหล่านี้อาจเพิ่มมูลค่าให้กับ OP :-) BTW การเขียนลักษณะที่ชอบis_f_ableเป็นสิ่งที่ฉันพิจารณางานสำหรับนักเขียนห้องสมุดที่สามารถใช้ประโยชน์จาก SFINAE ได้ แต่สำหรับผู้ใช้ "ปกติ" และได้รับคุณลักษณะis_f_ableฉันคิดว่าการส่งแท็กนั้นง่ายกว่า
TemplateRex

1
@ hansmaad ฉันโพสต์คำตอบสั้น ๆ เพื่อตอบคำถามของคุณและจะแก้ไขปัญหาของ "ถึง SFINAE หรือไม่ใช่ SFINAE" ในบล็อกโพสต์แทน (มันเป็นเรื่องเล็กน้อยสำหรับคำถามนี้) ทันทีที่ฉันได้เวลาเสร็จฉันก็หมายถึง
R. Martinho Fernandes

8
SFINAE นั้น "บอบบาง" หรือไม่? อะไร?
Lightness Races ที่ Orbit

5

โซลูชันใดควรเป็นที่ต้องการและทำไมฉันจึงควรหลีกเลี่ยงผู้อื่น

  • พารามิเตอร์เทมเพลต

    • มันสามารถใช้งานได้ในการก่อสร้าง
    • มันสามารถใช้งานได้ในผู้ประกอบการแปลงที่ผู้ใช้กำหนด
    • มันต้องมี C ++ 11 หรือใหม่กว่า
    • มันเป็น IMO ที่อ่านได้มากขึ้น
    • มันอาจถูกใช้อย่างไม่ถูกต้องและก่อให้เกิดข้อผิดพลาดเมื่อโอเวอร์โหลด:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()
      

    แจ้งให้ทราบล่วงหน้าtypename = std::enable_if_t<cond>แทนถูกต้องstd::enable_if_t<cond, int>::type = 0

  • ประเภทผลตอบแทน:

    • ไม่สามารถใช้ใน Constructor ได้ (ไม่มีประเภทผลตอบแทน)
    • ไม่สามารถใช้ในโอเปอเรเตอร์ที่ผู้ใช้กำหนด (ไม่สามารถหักได้)
    • สามารถใช้ pre-C ++ 11 ได้
    • IMO ที่อ่านได้มากขึ้นเป็นลำดับที่สอง
  • สุดท้ายในพารามิเตอร์ฟังก์ชัน:

    • สามารถใช้ pre-C ++ 11 ได้
    • มันสามารถใช้งานได้ในการก่อสร้าง
    • ไม่สามารถใช้ในโอเปอเรเตอร์ที่ผู้ใช้กำหนด (ไม่มีพารามิเตอร์)
    • มันไม่สามารถนำมาใช้ในวิธีการที่มีจำนวนคงที่ของการขัดแย้ง (เอก / ผู้ประกอบการไบนารี+, -, *, ... )
    • สามารถใช้ในการสืบทอดได้อย่างปลอดภัย (ดูด้านล่าง)
    • เปลี่ยนฟังก์ชันของลายเซ็น (โดยพื้นฐานแล้วคุณจะมีอาร์กิวเมนต์พิเศษเป็นพิเศษvoid* = nullptr) (ดังนั้นตัวชี้ฟังก์ชันจะแตกต่างกันไปเรื่อย ๆ )

มีความแตกต่างสำหรับแม่แบบฟังก์ชันสมาชิกและไม่ใช่สมาชิกหรือไม่?

มีความแตกต่างเล็กน้อยกับการสืบทอดและusing:

ตามusing-declarator(เหมืองเน้น):

namespace.udecl

ชุดของการประกาศที่มีการประกาศโดย using-declarator นั้นพบได้จากการดำเนินการค้นหาชื่อที่มีคุณสมบัติเหมาะสม ([basic.lookup.qual], [class.member.lookup]) สำหรับชื่อใน using-declarator โดยไม่รวมฟังก์ชันที่ซ่อนอยู่ตามที่อธิบายไว้ ด้านล่าง

...

เมื่อ using-declarator นำการประกาศจากคลาสพื้นฐานมาเป็นคลาสที่ได้รับฟังก์ชันสมาชิกและแม่แบบฟังก์ชันสมาชิกในการแทนที่คลาสที่ได้รับและ / หรือซ่อนฟังก์ชันสมาชิกและแม่แบบฟังก์ชันสมาชิกด้วยชื่อเดียวกันพารามิเตอร์ -type-list cv- คุณสมบัติและ ref-qualifier (ถ้ามี) ในคลาสพื้นฐาน (แทนที่จะเป็นข้อขัดแย้ง) การประกาศที่ซ่อนอยู่หรือที่ถูกยกเลิกดังกล่าวจะถูกแยกออกจากชุดของการประกาศที่ใช้โดย declarator

ดังนั้นสำหรับทั้งเท็มเพลตอาร์กิวเมนต์และชนิดส่งคืนเมธอดถูกซ่อนอยู่จึงเป็นสถานการณ์ต่อไปนี้:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

การสาธิต (gcc ค้นหาฟังก์ชันฐานอย่างผิดพลาด)

ในขณะที่มีการโต้เถียง

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

การสาธิต

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