ทำไม C ++ deduce T ในการโทรไปยัง Foo <T> :: Foo (T&&) ไม่ได้?


9

รับโครงสร้างแม่แบบต่อไปนี้:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

การคอมไพล์นี้และTถูกอนุมานว่าเป็นint:

auto f = Foo(2);

แต่สิ่งนี้ไม่ได้รวบรวม: https://godbolt.org/z/hAA9TE

int x = 2;
auto f = Foo(x);

/*
<source>:12:15: error: no viable constructor or deduction guide for deduction of template arguments of 'Foo'
    auto f = Foo(x);
             ^

<source>:7:5: note: candidate function [with T = int] not viable: no known conversion from 'int' to 'int &&' for 1st argument
    Foo(T&&) {}
    ^
*/

อย่างไรก็ตามFoo<int&>(x)เป็นที่ยอมรับ

แต่เมื่อฉันเพิ่มคำแนะนำที่ผู้ใช้กำหนดเองซ้ำซ้อนซ้ำซ้อนมันทำงานได้:

template<typename T>
Foo(T&&) -> Foo<T>;

เหตุใดจึงไม่Tสามารถอนุมานได้ว่าint&ไม่มีคู่มือการหักที่ผู้ใช้กำหนด?



คำถามนั้นดูเหมือนจะเกี่ยวกับประเภทเทมเพลตเช่นFoo<T<A>>
jtbandes

คำตอบ:


5

ฉันคิดว่าความสับสนเกิดขึ้นที่นี่เพราะมีข้อยกเว้นเฉพาะสำหรับคู่มือการหักสังเคราะห์ที่เกี่ยวข้องกับการอ้างอิงการส่งต่อ

มันเป็นความจริงที่ฟังก์ชั่นผู้สมัครสำหรับจุดประสงค์ของการลดอาร์กิวเมนต์เท็มเพลตคลาสที่สร้างจากตัวสร้างและสิ่งที่สร้างจากคู่มือการหักที่ผู้ใช้กำหนดมีลักษณะเหมือนกันเช่น

template<typename T>
auto f(T&&) -> Foo<T>;

แต่สำหรับสิ่งที่สร้างขึ้นจากนวกรรมิกT&&เป็นการอ้างอิง rvalue อย่างง่ายในขณะที่เป็นการอ้างอิงการส่งต่อในกรณีที่ผู้ใช้กำหนด สิ่งนี้ถูกระบุโดย[temp.deduct.call] / 3ของมาตรฐาน C ++ 17 (ฉบับร่าง N4659 เน้นการทำเหมือง):

อ้างอิงส่งต่อคือการอ้างอิง rvalue กับ CV-ไม่มีเงื่อนไขพารามิเตอร์แม่แบบที่ไม่ได้เป็นตัวแทนของพารามิเตอร์แม่แบบของแม่แบบชั้นเรียน (ในชั้นเรียนหักแม่แบบอาร์กิวเมนต์ ([over.match.class.deduct]))

ดังนั้นผู้สมัครที่สังเคราะห์จากสร้างชั้นจะไม่ได้ข้อสรุปTเช่นถ้าจากการอ้างอิงการส่งต่อ (ซึ่งอาจอนุมานTที่จะอ้างอิง lvalue เพื่อให้T&&ยังมีการอ้างอิง lvalue) แต่จะได้ข้อสรุปTว่าไม่สามารถอ้างอิงเพื่อให้T&&อยู่เสมอ การอ้างอิงค่า rvalue


1
ขอบคุณสำหรับคำตอบที่ชัดเจนและรัดกุม คุณรู้หรือไม่ว่าทำไมมีข้อยกเว้นสำหรับพารามิเตอร์แม่แบบคลาสในกฎสำหรับการส่งต่อการอ้างอิง?
jtbandes

2
@ jtbandes นี้ดูเหมือนว่าจะมีการเปลี่ยนแปลงเป็นผลมาจากความคิดเห็นโดยร่างกายแห่งชาติของสหรัฐฯดูกระดาษp0512r0 ฉันไม่พบความคิดเห็น ฉันเดาเหตุผลก็คือว่าถ้าคุณเขียนนวกรรมิกที่มีการอ้างอิง rvalue คุณมักจะคาดหวังให้มันทำงานในลักษณะเดียวกับที่คุณระบุFoo<int>(...)หรือเพียงแค่Foo(...)ซึ่งไม่ใช่กรณีที่มีการส่งต่อการอ้างอิง (ซึ่งอาจอนุมานFoo<int&>แทน
walnut

6

ปัญหาคือที่นี่เนื่องจากคลาสถูกเทมเพลตTในคอนสตรัคเตอร์Foo(T&&)เราไม่ได้ทำการลดการพิมพ์ เรามีการอ้างอิงค่า r เสมอ นั่นคือคอนสตรัคสำหรับที่Fooมีลักษณะเช่นนี้:

Foo(int&&)

Foo(2)ทำงานเพราะ2เป็นที่แพร่หลาย

Foo(x)ไม่ได้เพราะxเป็น lvalue int&&ที่ไม่สามารถเชื่อมโยงกับ คุณสามารถทำได้std::move(x)เพื่อแปลงเป็นประเภทที่เหมาะสม ( สาธิต )

Foo<int&>(x)ทำงานได้ดีเพราะตัวสร้างกลายเป็นFoo(int&)เพราะการอ้างอิงกฎการยุบ เริ่มแรกมันFoo((int&)&&)ยุบตัวลงFoo(int&)ตามมาตรฐาน

ในส่วนที่เกี่ยวกับคำแนะนำการหัก "ซ้ำซ้อน" ของคุณ: เริ่มแรกมีคำแนะนำการหักแม่แบบเริ่มต้นสำหรับรหัสที่โดยทั่วไปทำหน้าที่เหมือนฟังก์ชันผู้ช่วยดังนี้

template<typename T>
struct Foo {
    Foo(T&&) {}
};

template<typename T>
Foo<T> MakeFoo(std::add_rvalue_reference_t<T> value)
{
   return Foo<T>(std::move(value));
}

//... 
auto f = MakeFoo(x);

นี่เป็นเพราะมาตรฐานกำหนดว่าวิธีแม่แบบนี้ (ตัวละคร) มีพารามิเตอร์เทมเพลตเดียวกับคลาส (เพิ่งT) ตามด้วยพารามิเตอร์แม่แบบใด ๆ ที่เป็นตัวสร้าง (ไม่มีในกรณีนี้ตัวสร้างไม่ได้สร้างเทมเพลต) จากนั้นชนิดของพารามิเตอร์ฟังก์ชั่นจะเหมือนกันกับที่อยู่ในตัวสร้าง ในกรณีของเราหลังจาก instantiating คอนFoo<int>สตรัคดูเหมือนFoo(int&&)การอ้างอิง rvalue ในคำอื่น ๆ ดังนั้นการใช้งานของadd_rvalue_reference_tข้างต้น

เห็นได้ชัดว่านี่ใช้งานไม่ได้

เมื่อคุณเพิ่มคู่มือการหัก "ซ้ำซ้อน" ของคุณ:

template<typename T>
Foo(T&&) -> Foo<T>;

คุณได้รับอนุญาตให้คอมไพเลอร์ที่จะแยกแยะว่าแม้จะมีชนิดของการอ้างอิงที่แนบมากับTในคอนสตรัค ( int&, const int&หรือint&&อื่น ๆ ) คุณตั้งใจประเภทอนุมานสำหรับชั้นเรียนที่จะเป็นโดยการอ้างอิง (เพียงT) นี่เป็นเพราะเรากำลังทำการอนุมานประเภท

ตอนนี้เราสร้างฟังก์ชันตัวช่วยอีกตัวหนึ่ง (ตัวละคร) ที่มีลักษณะดังนี้:

template<class U>
Foo<U> MakeFoo(U&& u)
{
   return Foo<U>(std::forward<U>(u));
}

// ...
auto f = MakeFoo(x);

(การเรียกของเราไปยังตัวสร้างจะถูกเปลี่ยนเส้นทางไปยังฟังก์ชันตัวช่วยสำหรับจุดประสงค์ของการลดอาร์กิวเมนต์เทมเพลตคลาสจึงFoo(x)กลายเป็นMakeFoo(x))

สิ่งนี้จะช่วยให้U&&กลายเป็นint&และTง่ายint


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

1
แต่ถ้า T ยังไม่ถูกอนุมาน "ประเภทใด ๆ ที่แปลงได้เป็น T" หมายถึงอะไร
jtbandes

1
คุณสามารถเสนอวิธีแก้ปัญหาได้อย่างรวดเร็ว แต่คุณสามารถมุ่งเน้นไปที่คำอธิบายเพิ่มเติมได้มากขึ้นว่าทำไมมันไม่ทำงานเว้นแต่คุณจะแก้ไขด้วยวิธีใดวิธีหนึ่ง ทำไมมันไม่ทำงานเหมือนที่เป็นอยู่? " xเป็น lvalue ที่ไม่สามารถผูกกับint&&" แต่คนที่ไม่เข้าใจจะงงที่Foo<int&>(x)สามารถทำงานได้ แต่ไม่ได้คิดออกโดยอัตโนมัติ - ฉันคิดว่าเราทุกคนต้องการความเข้าใจอย่างลึกซึ้งว่าทำไม
Wyck

2
@Wyck: ฉันได้อัปเดตโพสต์เพื่อมุ่งเน้นที่สาเหตุมากขึ้น
AndyG

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