คำตอบสั้น ๆ : เพื่อสร้างx
ชื่อที่ขึ้นต่อกันดังนั้นการค้นหาจะถูกเลื่อนออกไปจนกว่าจะรู้พารามิเตอร์เทมเพลต
คำตอบยาว: เมื่อคอมไพเลอร์เห็นแม่แบบมันควรทำการตรวจสอบบางอย่างทันทีโดยไม่เห็นพารามิเตอร์แม่แบบ อื่น ๆ จะถูกเลื่อนออกไปจนกว่าจะทราบพารามิเตอร์ มันเรียกว่าการคอมไพล์แบบสองเฟสและ MSVC ไม่ได้ทำ แต่มันต้องการโดยมาตรฐานและนำมาใช้โดยคอมไพเลอร์หลักอื่น ๆ หากคุณต้องการคอมไพเลอร์จะต้องรวบรวมเทมเพลตทันทีที่เห็น (เพื่อแสดงการแยกวิเคราะห์ต้นไม้ภายใน) และเลื่อนการรวบรวมอินสแตนซ์จนกระทั่งภายหลัง
การตรวจสอบที่ดำเนินการบนเทมเพลตเองแทนที่จะเป็นอินสแตนซ์เฉพาะของมันต้องการให้คอมไพเลอร์สามารถแก้ไขไวยากรณ์ของรหัสในเทมเพลต
ใน C ++ (และ C) เพื่อแก้ไขไวยากรณ์ของรหัสบางครั้งคุณจำเป็นต้องรู้ว่ามีบางอย่างเป็นประเภทหรือไม่ ตัวอย่างเช่น:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
ถ้า A เป็นประเภทนั่นจะประกาศตัวชี้ (โดยไม่มีเอฟเฟกต์อื่นนอกจากจะทำให้เป็นเงาส่วนกลางx
) ถ้า A เป็นวัตถุนั่นคือการคูณ (และ จำกัด การใช้งานตัวดำเนินการมากเกินไปมันผิดกฎหมายกำหนดให้ค่า rvalue) ถ้ามันผิดข้อผิดพลาดนี้จะต้องได้รับการวินิจฉัยในระยะที่ 1มันถูกกำหนดโดยมาตรฐานเพื่อให้เกิดข้อผิดพลาดในแม่แบบไม่ใช่ในบางส่วนของอินสแตนซ์ของมัน แม้ว่าเทมเพลตจะไม่สร้างอินสแตนซ์ถ้า A คือint
รหัสข้างต้นนั้นมีรูปแบบไม่ดีและต้องได้รับการวินิจฉัยเช่นเดียวกับถ้าfoo
ไม่ใช่เทมเพลตเลย แต่เป็นฟังก์ชั่นธรรมดา
ตอนนี้มาตรฐานกล่าวว่าชื่อที่ไม่ได้ขึ้นอยู่กับพารามิเตอร์แม่แบบจะต้อง resolvable ในขั้นตอนที่ 1 ที่นี่ไม่ได้ขึ้นอยู่กับชื่อมันหมายถึงสิ่งเดียวกันไม่คำนึงถึงประเภทA
T
ดังนั้นจึงจำเป็นต้องกำหนดก่อนที่จะมีการกำหนดเทมเพลตเพื่อให้สามารถค้นหาและตรวจสอบได้ในเฟส 1
T::A
จะเป็นชื่อที่ขึ้นอยู่กับ T เราไม่สามารถรู้ได้ในระยะที่ 1 ว่าเป็นประเภทหรือไม่ ประเภทที่จะใช้T
ในการสร้างอินสแตนซ์ในที่สุดก็ยังไม่ได้กำหนดและแม้ว่าเราจะไม่ทราบว่าจะใช้ประเภทใดเป็นพารามิเตอร์เทมเพลตของเรา แต่เราต้องแก้ไขไวยากรณ์เพื่อทำการตรวจสอบระยะที่ 1 อันมีค่าของเราสำหรับเทมเพลตที่ไม่ดี ดังนั้นมาตรฐานจึงมีกฎสำหรับชื่อที่ต้องพึ่งพากัน - คอมไพเลอร์จะต้องสมมติว่าพวกมันไม่ใช่ประเภทเว้นแต่จะผ่านการรับรองด้วยtypename
เพื่อระบุว่าพวกเขาเป็นประเภทหรือใช้ในบริบทที่ชัดเจนบางอย่าง ตัวอย่างเช่นในtemplate <typename T> struct Foo : T::A {};
, T::A
ใช้เป็นคลาสพื้นฐานและดังนั้นจึงเป็นประเภทที่ไม่น่าสงสัย ถ้าFoo
เป็นอินสแตนซ์ที่มีประเภทข้อมูลสมาชิกA
แทนที่จะเป็นชนิด A ที่ซ้อนกันนั่นเป็นข้อผิดพลาดในรหัสที่ทำการสร้างอินสแตนซ์ (เฟส 2) ไม่ใช่ข้อผิดพลาดในเทมเพลต (เฟส 1)
แต่แม่แบบชั้นเรียนที่มีคลาสพื้นฐานที่ขึ้นต่อกันคืออะไร
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
A เป็นชื่อที่ต้องพึ่งพาหรือไม่? ด้วยคลาสพื้นฐานชื่อใด ๆอาจปรากฏในคลาสพื้นฐาน ดังนั้นเราสามารถพูดได้ว่า A เป็นชื่อที่ต้องพึ่งพาและถือว่าเป็นประเภทที่ไม่เกี่ยวข้อง สิ่งนี้จะมีผลกระทบที่ไม่พึงประสงค์ที่ทุกชื่อใน Foo ขึ้นอยู่กับและดังนั้นทุกประเภทที่ใช้ใน Foo (ยกเว้นประเภทในตัว) จะต้องผ่านการรับรอง ภายใน Foo คุณต้องเขียน:
typename std::string s = "hello, world";
เพราะstd::string
จะเป็นชื่อที่ต้องพึ่งพาและด้วยเหตุนี้จึงถือว่าเป็นประเภทที่ไม่ใช่เว้นแต่จะระบุไว้เป็นอย่างอื่น อุ๊ย!
ปัญหาที่สองเกี่ยวกับการอนุญาตให้โค้ดที่คุณต้องการ ( return x;
) คือแม้ว่าBar
จะมีการกำหนดไว้ก่อนหน้าFoo
นี้และx
ไม่ใช่สมาชิกในคำจำกัดความนั้นบางคนสามารถกำหนดความเชี่ยวชาญพิเศษBar
สำหรับบางประเภทในภายหลังBaz
เช่นBar<Baz>
มีสมาชิกข้อมูลx
แล้วสร้างอินสแตนซ์Foo<Baz>
. x
ดังนั้นในการเริ่มที่แม่แบบของคุณจะกลับมาข้อมูลสมาชิกแทนการกลับโลก หรือตรงกันข้ามถ้านิยามแม่แบบฐานของBar
มีx
พวกเขาสามารถกำหนดความเชี่ยวชาญโดยไม่ได้และแม่แบบของคุณจะมองหาทั่วโลกจะกลับมาในx
Foo<Baz>
ฉันคิดว่าสิ่งนี้ได้รับการตัดสินว่าน่าประหลาดใจและน่าสังเวชเหมือนปัญหาที่คุณมี แต่มันเงียบ ๆ น่าประหลาดใจเมื่อเทียบกับการโยนข้อผิดพลาดที่น่าแปลกใจ
เพื่อหลีกเลี่ยงปัญหาเหล่านี้มาตรฐานที่มีผลบังคับใช้จะบอกว่าคลาสพื้นฐานที่ขึ้นอยู่กับเทมเพลตของคลาสนั้นไม่ได้ถูกนำมาพิจารณาสำหรับการค้นหาเว้นแต่จะมีการร้องขออย่างชัดเจน สิ่งนี้จะหยุดทุกสิ่งจากการพึ่งพาเพียงเพราะมันสามารถพบได้ในฐานที่พึ่งพา นอกจากนี้ยังมีเอฟเฟกต์ที่ไม่พึงประสงค์ที่คุณเห็นคุณต้องมีคุณสมบัติจากคลาสฐานหรือไม่พบ มีสามวิธีทั่วไปในการA
พึ่งพา:
using Bar<T>::A;
ในชั้นเรียน - A
ตอนนี้หมายถึงบางสิ่งบางอย่างในBar<T>
จึงขึ้นอยู่กับ
Bar<T>::A *x = 0;
ที่จุดของการใช้งาน - อีกครั้งแน่นอนในA
Bar<T>
นี่คือการคูณเนื่องจากtypename
ไม่ได้ใช้ดังนั้นอาจเป็นตัวอย่างที่ไม่ดี แต่เราจะต้องรอจนกว่าการสร้างอินสแตนซ์เพื่อค้นหาว่าoperator*(Bar<T>::A, x)
คืนค่า rvalue หรือไม่ ใครจะรู้บางทีมันอาจจะ ...
this->A;
at point of use - A
เป็นสมาชิกดังนั้นหากไม่ได้อยู่ในFoo
นั้นจะต้องอยู่ในระดับฐานอีกครั้งมาตรฐานบอกว่าสิ่งนี้ทำให้มันขึ้นอยู่กับ
การคอมไพล์แบบสองเฟสนั้นยุ่งยากและยุ่งยากและแนะนำข้อกำหนดที่น่าประหลาดใจบางประการสำหรับการใช้คำฟุ่มเฟือยในรหัสของคุณ แต่แทนที่จะเป็นประชาธิปไตยมันอาจเป็นวิธีที่เลวร้ายที่สุดในการทำสิ่งนอกเหนือจากคนอื่น ๆ ทั้งหมด
คุณอาจโต้เถียงอย่างสมเหตุสมผลว่าในตัวอย่างของคุณreturn x;
ไม่สมเหตุสมผลถ้าx
เป็นชนิดซ้อนกันในคลาสพื้นฐานดังนั้นภาษาควร (ก) บอกว่ามันเป็นชื่อที่ต้องพึ่งพาและ (2) ถือว่าเป็นประเภทที่ไม่ใช่และ รหัสของคุณจะใช้งานไม่this->
ได้ เท่าที่คุณตกเป็นเหยื่อของความเสียหายหลักประกันจากการแก้ปัญหาที่ไม่ได้ใช้ในกรณีของคุณ แต่ยังคงมีปัญหาของชั้นฐานของคุณอาจแนะนำชื่อภายใต้คุณที่เงากลมหรือไม่มีชื่อที่คุณคิด พวกเขามีและถูกพบทั่วโลกแทน
คุณอาจจะยังอาจจะเถียงว่าเริ่มต้นควรจะตรงข้ามสำหรับชื่อขึ้นอยู่กับ (สมมติประเภทนอกจากที่ระบุไว้อย่างใดที่จะเป็นวัตถุ) หรือว่าเริ่มต้นควรจะบริบทอื่น ๆ ที่สำคัญ (ในstd::string s = "";
, std::string
สามารถอ่านเป็นชนิดตั้งแต่ไม่มีอะไรอื่นทำให้ไวยากรณ์ แม้จะstd::string *s = 0;
คลุมเครือก็ตาม) อีกครั้งฉันไม่ทราบวิธีการตกลงกฎ ฉันเดาว่าจำนวนหน้าของข้อความที่จะต้องลดลงกับการสร้างกฎเฉพาะจำนวนมากที่บริบทใช้ประเภทและที่ไม่ใช่ประเภท