`void_t` ทำงานอย่างไร


149

ผมเฝ้าดูการพูดคุยวอลเตอร์บราวน์ที่ Cppcon14 เกี่ยวกับการเขียนโปรแกรมแม่แบบที่ทันสมัย ( Part I , Part II ) ที่เขานำเสนอของเขาvoid_tเทคนิค SFINAE

ตัวอย่าง:
กำหนดเทมเพลตตัวแปรแบบง่ายที่ประเมินvoidว่าถ้าอาร์กิวเมนต์เท็มเพลตทั้งหมดมีรูปแบบที่ดี:

template< class ... > using void_t = void;

และลักษณะต่อไปนี้ที่ตรวจสอบการมีอยู่ของตัวแปรสมาชิกที่เรียกว่าสมาชิก :

template< class , class = void >
struct has_member : std::false_type
{ };

// specialized as has_member< T , void > or discarded (sfinae)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };

ฉันพยายามเข้าใจว่าเพราะอะไรและทำงานอย่างไร ดังนั้นตัวอย่างเล็ก ๆ :

class A {
public:
    int member;
};

class B {
};

static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );

1 has_member< A >

  • has_member< A , void_t< decltype( A::member ) > >
    • A::member ที่มีอยู่
    • decltype( A::member ) มีรูปแบบที่ดี
    • void_t<> ถูกต้องและประเมินผลถึง void
  • has_member< A , void > และดังนั้นจึงเลือกเทมเพลตพิเศษ
  • has_member< T , void > และประเมินผลให้ true_type

2 has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member ไม่ได้อยู่
    • decltype( B::member ) มีรูปแบบไม่ดีและล้มเหลวอย่างเงียบ ๆ (sfinae)
    • has_member< B , expression-sfinae > เทมเพลตนี้จึงถูกยกเลิก
  • คอมไพเลอร์พบhas_member< B , class = void >กับโมฆะเป็นอาร์กิวเมนต์เริ่มต้น
  • has_member< B > ประเมินให้ false_type

http://ideone.com/HCTlBb

คำถาม:
1. ความเข้าใจของฉันเกี่ยวกับสิ่งนี้ถูกต้องหรือไม่?
2. วอลเตอร์บราวน์ระบุว่าอาร์กิวเมนต์เริ่มต้นจะต้องเป็นชนิดเดียวกันกับที่ใช้ในvoid_tการทำงาน ทำไมถึงเป็นอย่างนั้น? (ฉันไม่เห็นว่าทำไมประเภทนี้ต้องตรงกันไม่ใช่แค่งานประเภทเริ่มต้นเท่านั้น)


6
โฆษณา 2) has_member<A,int>::valueลองนึกภาพยืนยันคงเขียนเป็น: จากนั้นความเชี่ยวชาญเฉพาะบางส่วนที่ประเมินว่าhas_member<A,void>ไม่สามารถจับคู่ได้ ดังนั้นจะต้องมีหรือมีน้ำตาลประโยคอาร์กิวเมนต์เริ่มต้นของประเภทhas_member<A,void>::value void
dyp

1
@dyp ขอบคุณฉันจะแก้ไข Mh ผมไม่เห็นความจำเป็นในการมีhas_member< T , class = void >ผิดนัดในvoidเลย สมมติว่าคุณสมบัตินี้จะใช้กับอาร์กิวเมนต์ 1 เทมเพลตเมื่อใดก็ได้ดังนั้นอาร์กิวเมนต์เริ่มต้นอาจเป็นประเภทใดก็ได้
อนุมัติ

คำถามที่น่าสนใจ
สิ้นสุด

2
ทราบว่าในข้อเสนอนี้open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdfวอลเตอร์เปลี่ยนไปtemplate <class, class = void> template <class, class = void_t<>>ดังนั้นตอนนี้เรามีอิสระที่จะทำสิ่งที่เราต้องการด้วยvoid_tการดำเนินนามแฝงแม่แบบ :)
JohnKoch

คำตอบ:


133

1. แม่แบบชั้นประถมศึกษา

เมื่อคุณเขียนhas_member<A>::valueคอมไพเลอร์จะค้นหาชื่อhas_memberและค้นหาเทมเพลตคลาสหลักนั่นคือการประกาศนี้:

template< class , class = void >
struct has_member;

(ใน OP หมายถึงเขียนเป็นคำจำกัดความ)

รายการอาร์กิวเมนต์เท็มเพลต<A>ถูกเปรียบเทียบกับรายการพารามิเตอร์เทมเพลตของเทมเพลตหลักนี้ ตั้งแต่แม่แบบหลักมีสองพารามิเตอร์ voidแต่คุณให้เพียงหนึ่งพารามิเตอร์ที่เหลือจะผิดนัดการอาร์กิวเมนต์แม่แบบเริ่มต้น: has_member<A, void>::valueมันเหมือนกับว่าคุณได้เขียน

2. เทมเพลตเฉพาะชั้น

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

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

คอมไพเลอร์พยายามจับคู่อาร์กิวเมนต์เท็มเพลตA, voidกับรูปแบบที่กำหนดในความเชี่ยวชาญเฉพาะบางส่วน: Tและvoid_t<..>หนึ่งต่อหนึ่ง ก่อนอื่นจะทำการลบอาร์กิวเมนต์แม่แบบ ความเชี่ยวชาญบางส่วนด้านบนยังคงเป็นเทมเพลตที่มีเทมเพลต - พารามิเตอร์ที่ต้อง "เติม" โดยอาร์กิวเมนต์

รูปแบบแรกที่ ช่วยให้คอมไพเลอร์ที่จะอนุมานแม่แบบพารามิเตอร์T Tนี่คือการหักเพียงเล็กน้อย แต่พิจารณารูปแบบT const&ที่เรายังสามารถอนุมานTได้ สำหรับรูปแบบTและอาร์กิวเมนต์แม่แบบAเราได้ข้อสรุปว่าจะเป็นTA

ในรูปแบบที่สองเท็ void_t< decltype( T::member ) >มเพลตพารามิเตอร์Tปรากฏขึ้นในบริบทที่ไม่สามารถอนุมานได้จากอาร์กิวเมนต์เท็มเพลตใด ๆ

มีสองเหตุผลสำหรับสิ่งนี้:

  • การแสดงออกภายในdecltypeถูกแยกออกจากการหักล้างอาร์กิวเมนต์เทมเพลตอย่างชัดเจน ฉันเดาว่ามันเป็นเพราะมันซับซ้อนโดยพลการ

  • แม้ว่าเราจะใช้รูปแบบที่ไม่decltypeชอบvoid_t< T >ก็ตามการหักTเกิดขึ้นในเทมเพลตนามแฝงที่แก้ไขแล้ว นั่นคือเราแก้ไขแม่แบบนามแฝงและต่อมาพยายามอนุมานประเภทTจากรูปแบบผลลัพธ์ รูปแบบที่เกิด แต่เป็นvoidซึ่งไม่ได้ขึ้นอยู่กับและดังนั้นจึงไม่ช่วยให้เราสามารถหาประเภทที่เฉพาะเจาะจงสำหรับT Tสิ่งนี้คล้ายกับปัญหาทางคณิตศาสตร์ของการพยายามสลับฟังก์ชันคงที่ (ในแง่คณิตศาสตร์ของคำเหล่านั้น)

หักอาร์กิวเมนต์แม่แบบเสร็จเรียบร้อยแล้ว(*) , ตอนนี้อนุมานข้อโต้แย้งแม่แบบถูกเปลี่ยนตัว สิ่งนี้สร้างความเชี่ยวชาญที่มีลักษณะเช่นนี้:

template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };

void_t< decltype( A::member ) >สามารถประเมินชนิดได้แล้ว มันเป็นรูปแบบที่ดีหลังจากการทดแทนดังนั้นจึงไม่มีความล้มเหลวในการทดแทนเกิดขึ้น เราได้รับ:

template<>
struct has_member<A, void> : true_type
{ };

3. ทางเลือก

ตอนนี้has_member<A>::valueเราสามารถเปรียบเทียบรายการแม่แบบพารามิเตอร์ของความเชี่ยวชาญนี้กับข้อโต้แย้งแม่แบบที่ให้มากับต้นฉบับ ทั้งสองประเภทตรงกันทั้งหมดดังนั้นจึงเลือกใช้ความเชี่ยวชาญเฉพาะบางส่วนนี้


ในทางกลับกันเมื่อเรากำหนดเทมเพลตเป็น:

template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

เราจบลงด้วยความเชี่ยวชาญที่เหมือนกัน:

template<>
struct has_member<A, void> : true_type
{ };

แต่รายการอาร์กิวเมนต์แม่แบบของเราสำหรับในขณะนี้คือhas_member<A>::value <A, int>อาร์กิวเมนต์ไม่ตรงกับพารามิเตอร์ของความเชี่ยวชาญและแม่แบบหลักถูกเลือกเป็นแบบย้อนกลับ


(*) Standard, IMHO อย่างสับสนรวมถึงกระบวนการทดแทนและการจับคู่ของข้อโต้แย้งแม่แบบที่ระบุไว้อย่างชัดเจนในกระบวนการหักอาร์กิวเมนต์แม่แบบ ตัวอย่างเช่น (post-N4296) [temp.class.spec.match] / 2:

ความเชี่ยวชาญเฉพาะบางส่วนตรงกับรายการอาร์กิวเมนต์แม่แบบที่แท้จริงที่กำหนดหากอาร์กิวเมนต์แม่แบบของความเชี่ยวชาญเฉพาะทางบางส่วนสามารถอนุมานได้จากรายการอาร์กิวเมนต์แม่แบบที่แท้จริง

แต่นี้ไม่ได้เป็นเพียงแค่หมายความว่าแม่แบบพารามิเตอร์ทั้งหมดของความเชี่ยวชาญบางส่วนจะต้องมีการอนุมาน; นอกจากนี้ยังหมายความว่าการทดแทนต้องสำเร็จและ (ตามที่ปรากฏ) อาร์กิวเมนต์ของเทมเพลตต้องตรงกับพารามิเตอร์เทมเพลต (ทดแทน) ของความเชี่ยวชาญเฉพาะบางส่วน โปรดทราบว่าฉันไม่ทราบว่ามาตรฐานระบุการเปรียบเทียบระหว่างรายการอาร์กิวเมนต์ที่ถูกแทนที่และรายการอาร์กิวเมนต์ที่ให้มา


3
ขอบคุณ! ฉันได้อ่านซ้ำแล้วซ้ำอีกและฉันคิดว่าฉันคิดว่าการหักล้างอาร์กิวเมนต์เทมเพลตทำงานอย่างไรและสิ่งที่คอมไพเลอร์เลือกสำหรับเทมเพลตสุดท้ายนั้นไม่ถูกต้อง
อนุมัติ

1
@ JohannesSchaub-litb ขอบคุณ! แม้ว่ามันจะค่อนข้างแย่ ไม่มีกฎสำหรับการจับคู่อาร์กิวเมนต์เท็มเพลตกับความเชี่ยวชาญหรือไม่? ไม่แม้แต่สำหรับความเชี่ยวชาญที่ชัดเจน?
DYP

2
อาร์กิวเมนต์เทมเพลตเริ่มต้นที่ไม่มี W / r / t open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC

1
@dyp ไม่กี่สัปดาห์ต่อมาและอ่านมากเกี่ยวกับเรื่องนี้และด้วยคำใบ้จากตัวอย่างนี้ฉันคิดว่าฉันเริ่มเข้าใจวิธีการทำงาน คำอธิบายของคุณทำให้ฉันอ่านมากขึ้นถึงความรู้สึกขอบคุณมาก!
ชดเชย

1
ฉันต้องการเพิ่มว่าเทมเพลตหลักคือคีย์ (เทมเพลตแรกพบในโค้ด)
ชดเชย

18
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

ความเชี่ยวชาญเฉพาะด้านบนนั้นเกิดขึ้นเมื่อมีรูปแบบที่ดีดังนั้นเมื่อdecltype( T::member )ถูกต้องและไม่คลุมเครือ ความเชี่ยวชาญจึงhas_member<T , void>เป็นเช่นเดียวกับรัฐในการแสดงความคิดเห็น

เมื่อคุณเขียนhas_member<A>อาจเป็นhas_member<A, void>เพราะอาร์กิวเมนต์แม่แบบเริ่มต้น

และเรามีความเชี่ยวชาญสำหรับhas_member<A, void>(สืบทอดมาจากtrue_type) แต่เราไม่มีความเชี่ยวชาญสำหรับhas_member<B, void>(ดังนั้นเราจึงใช้นิยามเริ่มต้น: สืบทอดมาจากfalse_type)

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