เหตุใดฉันจึงต้องเข้าถึงสมาชิกคลาสฐานแม่แบบผ่านตัวชี้นี้


199

หากคลาสด้านล่างไม่ใช่เทมเพลตฉันสามารถมีได้เฉพาะxในderivedคลาส แต่ด้วยรหัสข้างล่างนี้ผมต้องthis->xใช้ ทำไม?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}

1
อ่า jeez มันมีบางอย่างเกี่ยวกับการค้นหาชื่อ หากใครบางคนไม่ตอบคำถามนี้ในไม่ช้าฉันจะค้นหามันและโพสต์มัน (ยุ่งตอนนี้)
templatetypedef

@Ed Swangren: ขออภัยฉันคิดถึงคำตอบที่เสนอเมื่อโพสต์คำถามนี้ ฉันหาคำตอบมานานแล้วก่อนหน้านั้น
Ali

6
สิ่งนี้เกิดขึ้นเนื่องจากการค้นหาชื่อสองเฟส (ซึ่งไม่ใช่ตัวรวบรวมทั้งหมดที่ใช้เป็นค่าเริ่มต้น) และชื่อที่ขึ้นต่อกัน มี 3 แนวทางในการแก้ปัญหานี้อื่น ๆ นอกเหนือจากที่มีคำขึ้นต้นxด้วยthis->กล่าวคือ: 1)ใช้คำนำหน้าbase<T>::x, 2)เพิ่มคำสั่งusing base<T>::x, 3)ใช้สวิทช์คอมไพเลอร์ระดับโลกที่ช่วยให้โหมดอนุญาต ข้อดีและข้อเสียของการแก้ปัญหาเหล่านี้ได้อธิบายไว้ในstackoverflow.com/questions/50321788/…
George Robinson

คำตอบ:


274

คำตอบสั้น ๆ : เพื่อสร้าง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;คลุมเครือก็ตาม) อีกครั้งฉันไม่ทราบวิธีการตกลงกฎ ฉันเดาว่าจำนวนหน้าของข้อความที่จะต้องลดลงกับการสร้างกฎเฉพาะจำนวนมากที่บริบทใช้ประเภทและที่ไม่ใช่ประเภท


1
โอ้คำตอบที่ดีอย่างละเอียด ชี้แจงสองสิ่งที่ฉันไม่เคยสนใจที่จะมองหา :) +1
jalf

20
@alf: มีสิ่งเช่น C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "คำถามที่จะถูกถามบ่อยยกเว้นว่าคุณไม่แน่ใจว่าคุณต้องการทราบคำตอบและมีสิ่งที่สำคัญกว่าที่จะได้รับ"?
Steve Jessop

4
คำตอบพิเศษสงสัยว่าคำถามจะพอดีกับคำถามที่พบบ่อย
Matthieu M.

ว้าวเราสามารถพูดสารานุกรมได้ไหม highfive One subtle point แต่: "ถ้า Foo ถูกสร้างอินสแตนซ์ด้วยบางประเภทที่มีข้อมูลสมาชิก A แทนที่จะเป็นชนิดซ้อนกัน A นั่นเป็นข้อผิดพลาดในรหัสที่สร้างอินสแตนซ์ (เฟส 2) ไม่ใช่ข้อผิดพลาดในเทมเพลต (เฟส 1)." อาจจะดีกว่าถ้าจะบอกว่าแม่แบบนั้นไม่ผิดรูปแบบ แต่นี่อาจเป็นกรณีของการสันนิษฐานที่ไม่ถูกต้องหรือข้อผิดพลาดเชิงตรรกะในส่วนของตัวเขียนเทมเพลต หากการสร้างอินสแตนซ์ที่ถูกตั้งค่าสถานะเป็น usecase ที่ตั้งใจจริงแล้วเทมเพลตจะผิด
Ionoclast Brigham

1
@JohnH เนื่องจากคอมไพเลอร์หลาย ๆ ตัวนำไปใช้-fpermissiveหรือคล้ายกัน ผมไม่ทราบรายละเอียดของวิธีการที่จะดำเนินการ แต่คอมไพเลอร์จะต้องชะลอการแก้ไขจนกว่าจะรู้ชั้นฐานx tempate ที่เกิดขึ้นจริง Tดังนั้นในหลักการในโหมดที่ไม่ได้รับอนุญาตมันสามารถบันทึกความจริงที่มีการเลื่อนออกไปรอจนกว่าจะทำการค้นหาทันทีที่มีTและเมื่อการค้นหาประสบความสำเร็จก็จะออกข้อความที่คุณแนะนำ มันจะเป็นข้อเสนอแนะที่ถูกต้องหากทำในกรณีที่ใช้งานได้: โอกาสที่ผู้ใช้หมายถึงบางส่วนxจากขอบเขตอื่นนั้นค่อนข้างเล็ก!
Steve Jessop

13

(คำตอบดั้งเดิมจาก 10 มกราคม 2011)

ฉันคิดว่าฉันได้พบคำตอบ: ปัญหา GCC: ใช้เป็นสมาชิกของชั้นฐานที่ขึ้นอยู่กับการโต้แย้งแม่แบบ คำตอบไม่ได้เฉพาะเจาะจงกับ gcc


ปรับปรุง:เพื่อตอบสนองต่อความคิดเห็นของ mmichaelจากร่าง N3337ของมาตรฐาน C ++ 11:

14.6.2 ชื่อที่ขึ้นอยู่กับ [temp.dep]
[... ]
3 ในคำจำกัดความของแม่แบบคลาสหรือคลาสหากคลาสฐานขึ้นอยู่กับแม่แบบพารามิเตอร์พารามิเตอร์ขอบเขตคลาสฐานจะไม่ตรวจสอบในระหว่างการค้นหาชื่ออย่างไม่มีเงื่อนไขที่ จุดของคำนิยามของแม่แบบชั้นเรียนหรือสมาชิกหรือในระหว่างการเริ่มต้นของแม่แบบชั้นเรียนหรือสมาชิก

ไม่ว่า"เพราะมาตรฐานพูดอย่างนั้น" ก็นับเป็นคำตอบฉันไม่รู้ ตอนนี้เราสามารถถามได้ว่าทำไมคำสั่งมาตรฐานจึงเป็นเช่นนั้น แต่เมื่อคำตอบที่ยอดเยี่ยมของ Steve Jessopและคนอื่น ๆ ชี้ให้เห็นคำตอบของคำถามหลังนี้ค่อนข้างยาวและสามารถโต้แย้งได้ น่าเสียดายที่เมื่อพูดถึงมาตรฐาน C ++ มักจะเป็นไปไม่ได้เลยที่จะให้คำอธิบายสั้น ๆ และมีอยู่ในตัวเองว่าทำไมมาตรฐานจึงมีความสำคัญ นี้ใช้กับคำถามหลังเช่นกัน


11

xถูกซ่อนอยู่ในระหว่างการรับมรดก คุณสามารถยกเลิกการซ่อนผ่าน:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};

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