ฉันต้องใส่คำหลัก“ เทมเพลต” และ“ พิมพ์ชื่อ” ที่ไหนและทำไม


1125

ในแม่ที่และทำไมฉันต้องใส่typenameและtemplateรายชื่อขึ้นอยู่?
ชื่อที่ขึ้นต่อกันคืออะไรกันแน่?

ฉันมีรหัสต่อไปนี้:

template <typename T, typename Tail> // Tail will be a UnionNode too.
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        // Q: where to add typename/template here?
        typedef Tail::inUnion<U> dummy; 
    };
    template< > struct inUnion<T> {
    };
};
template <typename T> // For the last node Tn.
struct UnionNode<T, void> {
    // ...
    template<typename U> struct inUnion {
        char fail[ -2 + (sizeof(U)%2) ]; // Cannot be instantiated for any U
    };
    template< > struct inUnion<T> {
    };
};

ปัญหาที่ฉันมีอยู่ในtypedef Tail::inUnion<U> dummyบรรทัด ฉันค่อนข้างแน่ใจว่าinUnionเป็นชื่อที่ขึ้นต่อกันและ VC ++ ค่อนข้างถูกต้องในการสำลัก
ฉันก็รู้ว่าฉันควรจะสามารถเพิ่มtemplateบางแห่งเพื่อบอกคอมไพเลอร์ว่า inUnion เป็นเทมเพลต - ไอดี แต่ที่ไหนกันแน่? และควรจะสมมติว่า inUnion เป็นเทมเพลตคลาสเช่นinUnion<U>ตั้งชื่อประเภทและไม่ใช่ฟังก์ชันหรือไม่


1
คำถามที่น่ารำคาญ: ทำไมไม่เพิ่ม :: Variant?
Assaf Lavie

58
ความอ่อนไหวทางการเมืองการพกพา
MSalters

5
ฉันสร้างคำถามที่แท้จริงของคุณ ("วางเทมเพลต / ชื่อพิมพ์ที่ใด") โดดเด่นกว่าโดยใส่คำถามและรหัสสุดท้ายที่จุดเริ่มต้นและย่อโค้ดในแนวนอนให้พอดีกับหน้าจอ 1024x
Johannes Schaub - litb

7
นำ "ชื่อที่ต้องพึ่งพา" ออกจากชื่อเนื่องจากปรากฏว่าคนส่วนใหญ่ที่สงสัยเกี่ยวกับ "typename" และ "แม่แบบ" ไม่ทราบว่า "ชื่อที่ขึ้นต่อกัน" คืออะไร มันน่าสับสนน้อยลงสำหรับพวกเขาในวิธีนี้
Johannes Schaub - litb

2
@MSalters: การเพิ่มความสะดวกในการพกพา ฉันจะบอกว่ามีเพียงการเมืองเท่านั้นที่เป็นเหตุผลทั่วไปว่าทำไมการเพิ่มประสิทธิภาพจึงไม่ได้รับการยกเว้น เหตุผลเดียวที่ฉันรู้คือเวลาสร้างเพิ่มขึ้น ไม่เช่นนั้นจะเป็นการสูญเสียเงินหลายพันดอลลาร์เพื่อพลิกโฉมพวงมาลัย
v.oddou

คำตอบ:


1163

(ดูที่นี่สำหรับคำตอบ C ++ 11 ของฉันด้วย )

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

t * f;

ควรแยกวิเคราะห์นี้อย่างไร สำหรับหลาย ๆ ภาษาคอมไพเลอร์ไม่จำเป็นต้องทราบความหมายของชื่อเพื่อที่จะวิเคราะห์และรู้ว่าการกระทำของบรรทัดโค้ดคืออะไร ใน C ++ ข้างต้นสามารถให้การตีความที่แตกต่างกันอย่างมากมายโดยขึ้นอยู่กับความtหมาย fถ้าเป็นประเภทแล้วมันจะมีการประกาศตัวชี้ อย่างไรก็ตามถ้ามันไม่ใช่ประเภทมันจะเป็นการคูณ ดังนั้นมาตรฐาน C ++ กล่าวไว้ในย่อหน้า (3/7):

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

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

คุณอาจแนะนำให้รอจนกว่าผู้ใช้จะเริ่มต้นเทมเพลต:

รอ Let 's จนกว่าผู้ใช้จะ instantiates t::x * f;แม่แบบแล้วภายหลังพบความหมายที่แท้จริงของ

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

ดังนั้นจะต้องมีวิธีที่จะบอกคอมไพเลอร์ว่าชื่อบางชื่อเป็นประเภทและชื่อบางชื่อไม่ได้

คำหลัก "typename"

คำตอบคือ: เราตัดสินใจว่าคอมไพเลอร์ควรแยกวิเคราะห์นี้อย่างไร หากt::xเป็นชื่อที่ขึ้นต่อกันเราจำเป็นต้องนำหน้าด้วยtypenameเพื่อบอกคอมไพเลอร์ให้วิเคราะห์คำด้วยวิธีใดวิธีหนึ่ง มาตรฐานบอกว่าที่ (14.6 / 2):

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

มีหลายชื่อที่typenameไม่จำเป็นเนื่องจากคอมไพเลอร์สามารถใช้ชื่อการค้นหาที่เกี่ยวข้องในการกำหนดแม่แบบคิดออกว่าจะแยกวิเคราะห์การสร้างตัวเอง - ตัวอย่างเช่นด้วยT *f;เมื่อTเป็นพารามิเตอร์แม่แบบประเภท แต่สำหรับที่จะประกาศจะต้องมีการเขียนเป็นt::x * f; typename t::x *f;หากคุณไม่ใช้คำหลักและชื่อนั้นเป็นประเภทที่ไม่ใช่ แต่เมื่อเริ่มต้นพบว่ามันหมายถึงประเภทข้อความผิดพลาดตามปกติจะถูกปล่อยออกมาโดยคอมไพเลอร์ บางครั้งข้อผิดพลาดจะถูกกำหนดตามเวลาที่กำหนด:

// t::x is taken as non-type, but as an expression the following misses an
// operator between the two names or a semicolon separating them.
t::x f;

ไวยากรณ์อนุญาตให้typenameใช้ก่อนชื่อที่ผ่านการรับรองเท่านั้นซึ่งจะถูกนำมาใช้ตามที่ได้รับอนุญาตซึ่งชื่อที่ไม่ผ่านการรับรองนั้นเป็นที่รู้จักกันเสมอในการอ้างถึงชนิดหากพวกมันเป็นเช่นนั้น

gotcha ที่คล้ายกันมีอยู่สำหรับชื่อที่แสดงถึงเทมเพลตตามที่บอกไว้โดยข้อความเกริ่นนำ

คำหลัก "เทมเพลต"

จำคำพูดเริ่มต้นด้านบนและวิธีมาตรฐานต้องการการจัดการพิเศษสำหรับแม่แบบเช่นกัน? มาดูตัวอย่างที่ไร้เดียงสาต่อไปนี้:

boost::function< int() > f;

อาจดูเหมือนมนุษย์ผู้อ่าน ไม่ใช่สำหรับคอมไพเลอร์ ลองนึกภาพนิยามโดยพลการของboost::functionและf:

namespace boost { int function = 0; }
int main() { 
  int f = 0;
  boost::function< int() > f; 
}

นั่นเป็นนิพจน์ที่ถูกต้องจริงๆ! จะใช้น้อยกว่าผู้ประกอบการที่จะเปรียบเทียบboost::functionกับศูนย์ ( int()) แล้วใช้ที่มากขึ้นกว่าผู้ประกอบการที่จะเปรียบเทียบที่เกิดขึ้นกับbool fอย่างไรก็ตามอย่างที่คุณรู้boost::function ในชีวิตจริงเป็นเทมเพลตดังนั้นคอมไพเลอร์จึงรู้ (14.2 / 3):

หลังจากการค้นหาชื่อ (3.4) พบว่าชื่อนั้นเป็นชื่อเทมเพลตหากชื่อนี้ตามมาด้วย <, <จะถูกนำมาเป็นจุดเริ่มต้นของเทมเพลตอาร์กิวเมนต์รายการเสมอและไม่เคยเป็นชื่อที่ตามมาด้วยการน้อยกว่า - กว่าผู้ประกอบการ

typenameตอนนี้เราจะกลับไปที่ปัญหาเช่นเดียวกับที่มี จะเป็นอย่างไรถ้าเรายังไม่รู้ว่าชื่อนั้นเป็นเทมเพลตเมื่อทำการแยกวิเคราะห์รหัสหรือไม่ เราจะต้องใส่ทันทีก่อนที่ชื่อแม่แบบตามที่ระบุโดยtemplate 14.2/4ดูเหมือนว่า:

t::template f<int>(); // call a function template

ชื่อเทมเพลตไม่เพียง แต่จะเกิดขึ้นหลังจาก::แต่ยังหลังจาก->หรือ.ในการเข้าถึงสมาชิกระดับ คุณต้องแทรกคำหลักที่นั่นด้วย:

this->template f<int>(); // call a function template

การอ้างอิง

สำหรับคนที่มีหนังสือ Standardese หนา ๆ บนชั้นวางของและต้องการทราบว่าฉันกำลังพูดถึงอะไรฉันจะพูดถึงเรื่องนี้ที่ระบุไว้ใน Standard

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

มาตรฐานกำหนดกฎอย่างแม่นยำโดยการสร้างขึ้นอยู่กับหรือไม่ มันแยกพวกมันออกเป็นกลุ่มต่าง ๆ อย่างมีเหตุมีผล: ประเภทหนึ่งประเภทหนึ่งและอีกกลุ่มหนึ่งแสดงออก นิพจน์อาจขึ้นอยู่กับมูลค่าและ / หรือประเภทของพวกเขา ดังนั้นเราจึงมีตัวอย่างทั่วไปต่อท้าย:

  • ประเภทที่อ้างถึง (เช่น: พารามิเตอร์เทมเพลตชนิดT)
  • นิพจน์ที่ขึ้นกับมูลค่า (เช่น: พารามิเตอร์เทมเพลตที่ไม่ใช่ประเภทN)
  • นิพจน์ที่ขึ้นกับประเภท (เช่น: การส่งไปยังพารามิเตอร์เทมเพลตชนิด(T)0)

กฎส่วนใหญ่นั้นใช้งานง่ายและสร้างขึ้นแบบเรียกซ้ำ: ตัวอย่างชนิดที่สร้างT[N]ขึ้นตามประเภทที่ขึ้นกับว่าNเป็นนิพจน์ที่ขึ้นกับมูลค่าหรือTเป็นประเภทที่ขึ้นต่อกัน รายละเอียดของสิ่งนี้สามารถอ่านได้ในส่วน(14.6.2/1) สำหรับชนิดที่ขึ้นต่อกัน(14.6.2.2)สำหรับนิพจน์ที่ขึ้นอยู่กับประเภทและ(14.6.2.3)สำหรับนิพจน์ที่ขึ้นกับมูลค่า

ขึ้นอยู่กับชื่อ

มาตรฐานเป็นบิตไม่มีความชัดเจนเกี่ยวกับสิ่งที่ว่าเป็นชื่อที่ขึ้นอยู่กับ อ่านง่าย (คุณรู้ว่าหลักการของความประหลาดใจน้อยที่สุด) ทั้งหมดที่กำหนดเป็นชื่อขึ้นอยู่กับเป็นกรณีพิเศษสำหรับชื่อฟังก์ชั่นด้านล่าง แต่เนื่องจากT::xต้องมีการค้นหาอย่างชัดเจนในบริบทการสร้างอินสแตนซ์จึงต้องเป็นชื่อที่ต้องพึ่งพา (โชคดีที่ในช่วงกลาง C ++ 14 คณะกรรมการเริ่มมองหาวิธีแก้ไขคำจำกัดความที่สับสนนี้)

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

ชื่อคือการใช้ตัวระบุ (2.11), operator-function-id (13.5), conversion-function-id (12.3.2) หรือ template-id (14.2) ที่แสดงถึงเอนทิตีหรือป้ายกำกับ (6.6.4, 6.1)

ตัวระบุเป็นเพียงลำดับตัวอักษรธรรมดา / หลักในขณะที่อีกสองคือรูปแบบoperator +และ รูปแบบสุดท้ายคือoperator type template-name <argument list>ทั้งหมดเหล่านี้เป็นชื่อและโดยทั่วไปแล้วใช้ในมาตรฐานชื่อยังสามารถรวมตัวระบุที่บอกชื่อเนมสเปซหรือคลาสที่ควรค้นหา

นิพจน์ที่ขึ้นกับค่า1 + Nไม่ใช่ชื่อ แต่Nเป็น ชุดย่อยของโครงสร้างที่ขึ้นต่อกันทั้งหมดที่มีชื่อเรียกว่าชื่อที่ขึ้นต่อกัน อย่างไรก็ตามชื่อฟังก์ชั่นอาจมีความหมายแตกต่างกันในอินสแตนซ์ที่ต่างกันของเทมเพลต แต่น่าเสียดายที่กฎทั่วไปนี้ไม่ถูกตรวจจับ

ชื่อฟังก์ชั่นขึ้นอยู่กับ

ไม่ใช่ความกังวลของบทความนี้เป็นหลัก แต่ก็ยังมีมูลค่าการกล่าวขวัญ: ชื่อฟังก์ชั่นเป็นข้อยกเว้นที่ได้รับการจัดการแยกต่างหาก ชื่อฟังก์ชั่นตัวระบุไม่ได้ขึ้นอยู่กับตัวมันเอง แต่โดยการแสดงออกของอาร์กิวเมนต์ชนิดที่ขึ้นอยู่กับที่ใช้ในการโทร ในตัวอย่างf((T)0), fเป็นชื่อที่ขึ้นอยู่กับ (14.6.2/1)ในมาตรฐานนี้จะระบุไว้

หมายเหตุเพิ่มเติมและตัวอย่าง

ในกรณีที่เราต้องพอทั้งสองและtypename templateรหัสของคุณควรมีลักษณะดังนี้

template <typename T, typename Tail>
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        typedef typename Tail::template inUnion<U> dummy;
    };
    // ...
};

คำหลักtemplateไม่จำเป็นต้องปรากฏในส่วนสุดท้ายของชื่อเสมอไป สามารถปรากฏตรงกลางก่อนชื่อคลาสที่ใช้เป็นขอบเขตเช่นในตัวอย่างต่อไปนี้

typename t::template iterator<int>::value_type v;

ในบางกรณีคำหลักถูกห้ามดังรายละเอียดด้านล่าง

  • typenameที่ชื่อของชั้นฐานขึ้นอยู่กับคุณไม่ได้รับอนุญาตให้เขียน มันสันนิษฐานว่าชื่อที่ให้ไว้เป็นชื่อประเภทชั้นเรียน สิ่งนี้เป็นจริงสำหรับทั้งสองชื่อในรายการคลาสฐานและรายการตัวสร้างเริ่มต้น:

     template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
  • ในการใช้ - ประกาศเป็นไปไม่ได้ที่จะใช้templateหลังจากที่ผ่าน::มาและคณะกรรมการ C ++ บอกว่าจะไม่ทำงานในการแก้ปัญหา

     template <typename T>
     struct derive_from_Has_type : SomeBase<T> {
        using SomeBase<T>::template type; // error
        using typename SomeBase<T>::type; // typename *is* allowed
     };

22
คำตอบนี้ถูกคัดลอกจากรายการคำถามที่พบบ่อยก่อนหน้าของฉันซึ่งฉันได้ลบออกไปเพราะฉันพบว่าฉันควรใช้คำถามที่คล้ายกันมากกว่าเดิมแทนที่จะทำ "คำถามหลอก" ใหม่เพื่อจุดประสงค์ในการตอบคำถาม ขอบคุณไปที่@Prasoonผู้แก้ไขความคิดของส่วนสุดท้าย (กรณีที่ห้ามพิมพ์ชื่อ / แม่แบบ) ลงในคำตอบ
Johannes Schaub - litb

1
คุณสามารถช่วยฉันได้เมื่อใดที่ฉันควรใช้ไวยากรณ์นี้ this-> เทมเพลต f <int> (); ฉันได้รับข้อผิดพลาดนี้ 'แม่แบบ' (ในฐานะ disambiguator) อนุญาตเฉพาะในแม่แบบ แต่ไม่มีคำหลักของแม่แบบก็ใช้งานได้ดี
balki

1
ฉันถามคำถามที่คล้ายกันในวันนี้ที่ถูกทำเครื่องหมายโดยเร็วที่สุดเท่าที่ซ้ำกัน: stackoverflow.com/questions/27923722/... ฉันได้รับคำแนะนำให้ฟื้นคำถามนี้แทนที่จะสร้างคำถามใหม่ ฉันต้องบอกว่าฉันไม่เห็นด้วยกับการที่พวกเขาซ้ำซ้อน แต่ฉันเป็นใครใช่มั้ย ดังนั้นมีเหตุผลใดบ้างที่typenameบังคับใช้แม้ว่าไวยากรณ์จะไม่อนุญาตให้มีการตีความทางเลือกอื่นนอกเหนือจากชื่อประเภท ณ จุดนี้
JorenHeit

1
@ ปาโบลคุณไม่ได้พลาดอะไรเลย แต่ยังต้องเขียนความกำกวมแม้ว่าบรรทัดทั้งหมดจะไม่ชัดเจนอีกต่อไป
Johannes Schaub - litb

1
@Pablo มีวัตถุประสงค์เพื่อให้ภาษาและคอมไพเลอร์ง่ายขึ้น มีข้อเสนอที่จะอนุญาตให้สถานการณ์เพิ่มเติมคิดออกโดยอัตโนมัติเพื่อให้คุณต้องการคำหลักน้อยลง โปรดทราบว่าในตัวอย่างของคุณโทเค็นนั้นคลุมเครือและหลังจากที่คุณเห็น ">" หลังจากเป็นสองเท่าแล้วคุณสามารถแก้ปัญหานี้เป็นวงเล็บมุมเทมเพลตได้ สำหรับรายละเอียดเพิ่มเติมฉันเป็นคนผิดที่ต้องถามเพราะฉันไม่มีประสบการณ์ในการใช้ตัวแยกวิเคราะห์คอมไพเลอร์ของ C ++
Johannes Schaub - litb

135

C ++ 11

ปัญหา

ในขณะที่กฎใน C ++ 03 เกี่ยวกับเวลาที่คุณต้องการtypenameและtemplateมีเหตุผลส่วนใหญ่มีข้อเสียอย่างหนึ่งที่น่ารำคาญของการกำหนด

template<typename T>
struct A {
  typedef int result_type;

  void f() {
    // error, "this" is dependent, "template" keyword needed
    this->g<float>();

    // OK
    g<float>();

    // error, "A<T>" is dependent, "typename" keyword needed
    A<T>::result_type n1;

    // OK
    result_type n2; 
  }

  template<typename U>
  void g();
};

ดังที่เห็นได้เราจำเป็นต้องมีคำสำคัญที่ทำให้เข้าใจผิดแม้กระทั่งคอมไพเลอร์สามารถคิดได้อย่างสมบูรณ์ว่าA::result_typeสามารถint(และเป็นประเภท) และthis->gสามารถเป็นแม่แบบสมาชิกที่gประกาศในภายหลัง (แม้ว่าAจะมีความเชี่ยวชาญเฉพาะที่ ไม่ส่งผลกระทบต่อโค้ดภายในเทมเพลตนั้นดังนั้นความหมายของโค้ดจะไม่ได้รับผลกระทบจากความเชี่ยวชาญพิเศษของA!)

การเริ่มต้นปัจจุบัน

เพื่อปรับปรุงสถานการณ์ใน C ++ 11 แทร็กภาษาเมื่อชนิดอ้างถึงเทมเพลตที่ล้อมรอบ ที่จะรู้ว่าประเภทต้องได้รับการจัดตั้งขึ้นโดยใช้รูปแบบหนึ่งของชื่อซึ่งเป็นชื่อของตัวเอง (ในข้างต้นA, A<T>, ::A<T>) ประเภทอ้างอิงโดยชื่อดังกล่าวเป็นที่รู้จักกันเป็นinstantiation ปัจจุบัน อาจมีหลายประเภทที่เป็นอินสแตนซ์ปัจจุบันทั้งหมดหากประเภทที่ชื่อถูกสร้างขึ้นเป็นคลาสสมาชิก / ซ้อนกัน (จากนั้นA::NestedClassและAเป็นอินสแตนซ์ปัจจุบันทั้งคู่)

ขึ้นอยู่กับความคิดนี้ภาษาที่กล่าวว่าCurrentInstantiation::Foo, FooและCurrentInstantiationTyped->Foo(เช่นA *a = this; a->Foo) ทั้งหมดที่สมาชิกของ instantiation ปัจจุบัน ถ้าพวกเขาจะพบว่ามีสมาชิกของคลาสที่เป็น instantiation ปัจจุบันหรือหนึ่งในฐานเรียนที่ไม่ได้ขึ้นอยู่กับมัน (โดยเพียงแค่การทำ ค้นหาชื่อทันที)

คำหลักtypenameและtemplateไม่จำเป็นต้องใช้อีกต่อไปหากตัวระบุเป็นสมาชิกของการสร้างอินสแตนซ์ปัจจุบัน จุดสำคัญที่ต้องจำคือที่A<T>นี่ยังคงเป็นชื่อที่ขึ้นกับประเภท (หลังจากทั้งหมดTก็ขึ้นอยู่กับประเภทด้วย) แต่A<T>::result_typeเป็นที่รู้กันว่าเป็นประเภท - คอมไพเลอร์จะ "น่าอัศจรรย์" ดูประเภทนี้ขึ้นอยู่กับประเภทที่จะคิดออก

struct B {
  typedef int result_type;
};

template<typename T>
struct C { }; // could be specialized!

template<typename T>
struct D : B, C<T> {
  void f() {
    // OK, member of current instantiation!
    // A::result_type is not dependent: int
    D::result_type r1;

    // error, not a member of the current instantiation
    D::questionable_type r2;

    // OK for now - relying on C<T> to provide it
    // But not a member of the current instantiation
    typename D::questionable_type r3;        
  }
};

น่าประทับใจ แต่เราสามารถทำได้ดีกว่า ภาษาก็ไปต่อไปและต้องว่าการดำเนินการอีกครั้งเงยหน้าขึ้นD::result_typeเมื่อ instantiating D::f(แม้ว่าจะพบความหมายของมันอยู่แล้วในเวลาที่ความหมาย) เมื่อตอนนี้ผลการค้นหาแตกต่างกันหรือผลลัพธ์เป็นความกำกวมโปรแกรมจะมีรูปแบบไม่ดีและต้องได้รับการวินิจฉัย ลองจินตนาการว่าจะเกิดอะไรขึ้นถ้าเรานิยามCเช่นนี้

template<>
struct C<int> {
  typedef bool result_type;
  typedef int questionable_type;
};

เรียบเรียงเป็นสิ่งจำเป็นที่จะจับข้อผิดพลาดเมื่อ D<int>::finstantiating ดังนั้นคุณจะได้รับสิ่งที่ดีที่สุดของโลกทั้งสอง: "ล่าช้า" ค้นหาปกป้องคุณถ้าคุณสามารถได้รับในปัญหากับการเรียนฐานขึ้นและยัง "ทันที" การค้นหาที่ช่วยให้คุณจากและtypenametemplate

ความเชี่ยวชาญเฉพาะ

ในรหัสของDชื่อtypename D::questionable_typeไม่ได้เป็นสมาชิกของอินสแตนซ์ปัจจุบัน แทนเครื่องหมายภาษาเป็นสมาชิกคนหนึ่งของความเชี่ยวชาญที่ไม่รู้จัก โดยเฉพาะอย่างยิ่งกรณีนี้จะเกิดขึ้นเสมอเมื่อคุณทำDependentTypeName::FooหรือDependentTypedName->Fooประเภทที่ขึ้นต่อกันไม่ได้เป็นอินสแตนซ์ปัจจุบัน (ในกรณีที่คอมไพเลอร์สามารถยกเลิกได้และพูดว่า "เราจะดูทีหลังว่าFooมันคืออะไร) หรือเป็นอินสแตนซ์ปัจจุบัน ไม่พบชื่อในนั้นหรือคลาสพื้นฐานที่ไม่ขึ้นอยู่กับและยังมีคลาสฐานที่ขึ้นต่อกัน

ลองนึกภาพว่าจะเกิดอะไรขึ้นถ้าเรามีฟังก์ชั่นสมาชิกhภายในAเทมเพลตชั้นเรียนที่กำหนดไว้ด้านบน

void h() {
  typename A<T>::questionable_type x;
}

ใน C ++ 03 ภาษาอนุญาตให้จับข้อผิดพลาดนี้ได้เนื่องจากไม่มีวิธีที่ถูกต้องในการสร้างอินสแตนซ์A<T>::h(ไม่ว่าคุณจะโต้แย้งอะไรก็ตามT) ใน C ++ 11 ตอนนี้ภาษามีการตรวจสอบเพิ่มเติมเพื่อให้เหตุผลเพิ่มเติมสำหรับคอมไพเลอร์เพื่อใช้กฎนี้ เนื่องจากAไม่มีคลาสฐานที่ขึ้นต่อกันและAประกาศว่าไม่มีสมาชิกquestionable_typeชื่อA<T>::questionable_typeจึงไม่ใช่สมาชิกของการสร้างอินสแตนซ์ปัจจุบันหรือสมาชิกของความเชี่ยวชาญที่ไม่รู้จัก ในกรณีนั้นไม่ควรมีวิธีที่รหัสนั้นสามารถรวบรวมได้อย่างถูกต้องในเวลาเริ่มต้นดังนั้นภาษาห้ามชื่อที่มีคุณสมบัติเป็นการเริ่มต้นปัจจุบันที่จะไม่เป็นสมาชิกของความเชี่ยวชาญที่ไม่รู้จักหรือเป็นสมาชิกของการเริ่มต้นปัจจุบัน การละเมิดนี้ยังไม่จำเป็นต้องได้รับการวินิจฉัย)

ตัวอย่างและเรื่องไม่สำคัญ

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

กฎ C ++ 11 ทำให้โค้ด C ++ 03 ที่ถูกต้องต่อไปนี้ไม่ถูกต้อง (ซึ่งไม่ได้ถูกกำหนดโดยคณะกรรมการ C ++ แต่อาจไม่ได้รับการแก้ไข)

struct B { void f(); };
struct A : virtual B { void f(); };

template<typename T>
struct C : virtual B, T {
  void g() { this->f(); }
};

int main() { 
  C<A> c; c.g(); 
}

รหัส C ++ 03 ที่ถูกต้องนี้จะผูกthis->fกับA::fเวลาการเริ่มต้นและทุกอย่างเรียบร้อย อย่างไรก็ตาม C ++ 11 จะผูกกับมันทันทีB::fและต้องการการตรวจสอบอีกครั้งเมื่อสร้างอินสแตนซ์โดยตรวจสอบว่าการค้นหายังคงตรงกัน อย่างไรก็ตามเมื่ออินสแตนซ์C<A>::gที่กฎการปกครองนำไปใช้และการค้นหาจะพบA::fแทน


fyi - คำตอบนี้มีการอ้างอิงที่นี่: stackoverflow.com/questions/56411114/ ...... รหัสส่วนใหญ่ในคำตอบนี้ไม่ได้รวบรวมในคอมไพเลอร์ต่างๆ
Adam Rackis

@AdamRackis สมมติว่าสเป็ค C ++ ไม่ได้เปลี่ยนไปตั้งแต่ปี 2013 (วันที่ฉันเขียนคำตอบนี้) จากนั้นคอมไพเลอร์ที่คุณลองใช้รหัสของคุณก็ไม่ได้ใช้ C ++ 11 + -feature นี้เลย
Johannes Schaub - litb

99

คำนำ

โพสต์นี้จะหมายถึงการเป็นที่ง่ายต่อการอ่านทางเลือกในการโพสต์ของ litb

จุดประสงค์พื้นฐานเหมือนกัน; คำอธิบายของ "เมื่อใด" และทำไม?" typenameและtemplateจะต้องนำไปใช้


มีจุดประสงค์อะไรtypenameและtemplate?

typenameและtemplateสามารถใช้งานได้ในสถานการณ์อื่นนอกเหนือจากเมื่อประกาศแม่แบบ

มีบริบทบางอย่างในC ++ที่คอมไพเลอร์จะต้องบอกวิธีการปฏิบัติต่อชื่ออย่างชัดเจนและบริบททั้งหมดเหล่านี้มีสิ่งหนึ่งที่เหมือนกัน; พวกเขาขึ้นอยู่กับอย่างน้อยหนึ่งแม่แบบพารามิเตอร์

เราอ้างถึงชื่อดังกล่าวซึ่งอาจมีความกำกวมในการตีความเช่น; " ชื่อที่ขึ้นต่อกัน "

โพสต์นี้จะเสนอคำอธิบายเกี่ยวกับความสัมพันธ์ระหว่างชื่อที่ขึ้นกับและคำหลักสองคำ


SNIPPET บอกมากกว่า 1,000 คำ

พยายามอธิบายสิ่งที่เกิดขึ้นในฟังก์ชั่นเทมเพลตต่อไปนี้ไม่ว่าจะเป็นตัวคุณเองเพื่อนหรือแมวของคุณ เกิดอะไรขึ้นในข้อความระบุว่า ( A )

template<class T> void f_tmpl () { T::foo * x; /* <-- (A) */ }


มันอาจจะไม่ง่ายอย่างที่ใครคิดว่ามากขึ้นโดยเฉพาะผลของการประเมิน (คน) หนักขึ้นอยู่ในความหมายของประเภทที่ผ่านมาเป็นแม่แบบพารามิเตอร์T

หลายคนTสามารถเปลี่ยนความหมายที่เกี่ยวข้องอย่างมาก

struct X { typedef int       foo;       }; /* (C) --> */ f_tmpl<X> ();
struct Y { static  int const foo = 123; }; /* (D) --> */ f_tmpl<Y> ();


สถานการณ์ที่แตกต่างกันสองสถานการณ์ :

  • ถ้าเรายกตัวอย่างฟังก์ชั่นเทมเพลตด้วยประเภทXดังเช่นใน ( C ) เราจะมีการประกาศของตัวชี้ไปยัง intชื่อxแต่;

  • ถ้าเราสร้างอินสแตนซ์ของเทมเพลตด้วยประเภทYดังที่ใน ( D ), ( A ) จะประกอบด้วยนิพจน์ที่คำนวณผลิตภัณฑ์ที่123คูณด้วยตัวแปรที่ประกาศแล้วบางส่วน x



เหตุผล

มาตรฐาน C ++ ใส่ใจเกี่ยวกับความปลอดภัยและความเป็นอยู่ของเราอย่างน้อยในกรณีนี้

เพื่อป้องกันการดำเนินการจากความทุกข์ที่อาจเกิดขึ้นจากความประหลาดใจที่น่ารังเกียจเอกสารมาตรฐานที่เราจัดเรียงความคลุมเครือของชื่อที่ต้องพึ่งพาโดยระบุเจตนาอย่างชัดเจนทุกที่ที่เราต้องการรักษาชื่อเป็นชื่อ - ประเภทหรือเทมเพลต - รหัส

หากไม่มีการระบุชื่อใด ๆชื่อที่อ้างถึงจะถูกพิจารณาว่าเป็นตัวแปรหรือฟังก์ชัน



จะจัดการชื่อผู้สมัครได้อย่างไร

ถ้านี่เป็นภาพยนตร์ฮอลลีวูดชื่อที่ขึ้นกับคนนั้นจะเป็นโรคที่แพร่กระจายผ่านการสัมผัสทางร่างกายส่งผลกระทบต่อโฮสต์ของมันในทันที ความสับสนที่อาจเป็นไปได้นำไปสู่โปรแกรม perso- รูปแบบเอ่อ ..

ขึ้นชื่อคือใด ๆที่ชื่อโดยตรงหรือโดยอ้อมขึ้นอยู่กับแม่แบบพารามิเตอร์

template<class T> void g_tmpl () {
   SomeTrait<T>::type                   foo; // (E), ill-formed
   SomeTrait<T>::NestedTrait<int>::type bar; // (F), ill-formed
   foo.data<int> ();                         // (G), ill-formed    
}

เรามีชื่อที่ขึ้นต่อกันสี่ชื่อในตัวอย่างด้านบน:

  • )
    • "ประเภท"ขึ้นอยู่กับการเริ่มต้นของSomeTrait<T>ซึ่งรวมถึงTและ;
  • )
    • "NestedTrait"ซึ่งเป็นแม่แบบรหัสขึ้นอยู่กับSomeTrait<T>และ;
    • "type"ที่ส่วนท้ายของ ( F ) ขึ้นอยู่กับNestedTraitซึ่งขึ้นอยู่กับSomeTrait<T>และ;
  • )
    • "ข้อมูล"ซึ่งมีลักษณะเหมือนแม่แบบสมาชิกฟังก์ชั่นเป็นทางอ้อมขึ้นชื่อตั้งแต่ชนิดของfooSomeTrait<T>ขึ้นอยู่กับการเริ่มของ

ทั้ง statement ( E ), ( F ) หรือ ( G ) นั้นถูกต้องหากคอมไพเลอร์จะตีความชื่อที่ขึ้นกับว่าเป็นตัวแปร / ฟังก์ชั่น (ซึ่งตามที่ระบุไว้ก่อนหน้านี้คือสิ่งที่เกิดขึ้นหากเราไม่ได้

การแก้ไขปัญหา

เพื่อให้g_tmplมีคำจำกัดความที่ถูกต้องเราจะต้องบอกคอมไพเลอร์อย่างชัดเจนว่าเราคาดว่าจะเป็นประเภทใน ( E ), เทมเพลต - ไอดีและประเภทใน ( F ) และเทมเพลต - idใน ( G )

template<class T> void g_tmpl () {
   typename SomeTrait<T>::type foo;                            // (G), legal
   typename SomeTrait<T>::template NestedTrait<int>::type bar; // (H), legal
   foo.template data<int> ();                                  // (I), legal
}

เวลาทุกชื่อหมายถึงประเภท, ทุก ชื่อที่เกี่ยวข้องกับการต้องเป็นชื่อประเภทหรือnamespacesด้วยนี้ในใจมันเป็นเรื่องง่ายมากที่จะเห็นว่าเราใช้typenameจุดเริ่มต้นของอย่างเต็มที่ของเราชื่อที่มีคุณสมบัติเหมาะสม

templateอย่างไรก็ตามมีความแตกต่างในเรื่องนี้เนื่องจากไม่มีทางที่จะได้ข้อสรุปเช่น; "โอ้นี้เป็นแม่แบบนี้แล้วสิ่งอื่น ๆ ที่จะต้องเป็นแม่แบบ" ซึ่งหมายความว่าเราสมัครtemplateตรงหน้าชื่อที่เราต้องการจะปฏิบัติเช่นนี้



สามารถฉันติดKEYWORDSหน้าชื่อใด?

" ฉันเพียงแค่ติดtypenameและtemplateหน้าชื่อใด ๆ ที่ฉันไม่ต้องการที่จะกังวลเกี่ยวกับบริบทที่พวกเขาจะปรากฏ ... ? " -Some C++ Developer

กฎในมาตรฐานระบุว่าคุณสามารถใช้คำหลักได้ตราบใดที่คุณกำลังจัดการกับชื่อที่ผ่านการรับรอง ( K ) แต่หากชื่อนั้นไม่ผ่านการรับรองแอปพลิเคชันจะไม่ถูกต้อง ( L )

namespace N {
  template<class T>
  struct X { };
}

         N::         X<int> a; // ...  legal
typename N::template X<int> b; // (K), legal
typename template    X<int> c; // (L), ill-formed

หมายเหตุ : การใช้typenameหรือtemplateในบริบทที่ไม่จำเป็นไม่ถือว่าเป็นการปฏิบัติที่ดี เพียงเพราะคุณสามารถทำอะไรบางอย่างไม่ได้หมายความว่าคุณควร


นอกจากนี้ยังมีบริบทที่typenameและtemplateจะชัดเจนไม่ได้รับอนุญาต:

  • เมื่อระบุฐานของคลาสที่สืบทอด

    ทุกชื่อที่เขียนในรายการตัวระบุพื้นฐานของคลาสที่ได้รับจะถือว่าเป็นชื่อประเภทแล้วการระบุอย่างชัดเจนtypenameนั้นมีทั้งรูปแบบที่ไม่เหมาะสมและซ้ำซ้อน

                       // .------- the base-specifier-list
     template<class T> // v
     struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
       ...
     };


  • เมื่อtemplate-idเป็นสิ่งที่ถูกอ้างถึงในการใช้ directiveของคลาสที่ได้รับ

     struct Base {
       template<class T>
       struct type { };
     };
    
     struct Derived : Base {
       using Base::template type; // ill-formed
       using Base::type;          // legal
     };

20
typedef typename Tail::inUnion<U> dummy;

อย่างไรก็ตามฉันไม่แน่ใจว่าการติดตั้ง inUnion ของคุณนั้นถูกต้องหรือไม่ ถ้าฉันเข้าใจถูกต้องคลาสนี้ไม่ควรถูกสร้างอินสแตนซ์ดังนั้นแท็บ "ล้มเหลว" จะไม่ล้มเหลวโดยอัตโนมัติ อาจเป็นการดีกว่าถ้าระบุว่าประเภทอยู่ในสหภาพหรือไม่ด้วยค่าบูลีนแบบง่าย

template <typename T, typename TypeList> struct Contains;

template <typename T, typename Head, typename Tail>
struct Contains<T, UnionNode<Head, Tail> >
{
    enum { result = Contains<T, Tail>::result };
};

template <typename T, typename Tail>
struct Contains<T, UnionNode<T, Tail> >
{
    enum { result = true };
};

template <typename T>
struct Contains<T, void>
{
    enum { result = false };
};

PS: ดูที่Boost :: Variant

PS2: ดูนักพิมพ์ดีดโดยเฉพาะในหนังสือของ Andrei Alexandrescu: การออกแบบสมัยใหม่ C ++


inUnion <U> จะถูกสร้างอินสแตนซ์หากคุณพยายามโทรหา Union <float, bool> :: operator = (U) ด้วย U == int มันเรียกชุดส่วนตัว (U, inUnion <U> * = 0)
MSalters

และงานที่มี result = true / false คือฉันต้องการ boost :: enable_if <> ซึ่งไม่เข้ากันกับ toolchain OSX ปัจจุบันของเรา แม้ว่าแม่แบบแยกต่างหากยังคงเป็นความคิดที่ดี
MSalters

Luc หมายถึง typedef Tail :: inUnion <U> ดัมมี่ ไลน์. ที่จะยกตัวอย่างหาง แต่ไม่ใช่ใน UNION <U> มันจะได้รับอินสแตนซ์เมื่อต้องการความหมายเต็มรูปแบบของมัน ที่เกิดขึ้นเช่นถ้าคุณใช้ขนาดของหรือเข้าถึงสมาชิก (ใช้ :: foo) @Malters ต่อไปคุณมีปัญหาอีก:
Johannes Schaub - litb

-sizeof (U) ไม่เคยเป็นค่าลบ :) เนื่องจาก size_t เป็นประเภทจำนวนเต็มที่ไม่ได้ลงนาม คุณจะได้รับจำนวนที่สูงมาก คุณอาจต้องการทำ sizeof (U)> = 1? -1: 1 หรือคล้ายกัน :)
344902 Johannes Schaub - litb

ฉันจะปล่อยให้มันไม่ได้กำหนดและประกาศเท่านั้น: แม่แบบ <typename U> struct ในยูเนี่ยน; ดังนั้นจึงไม่สามารถสร้างอินสแตนซ์ได้อย่างแน่นอน ผมคิดว่ามันมีด้วย sizeof คอมไพเลอร์ยังได้รับอนุญาตเพื่อให้คุณมีข้อผิดพลาดแม้ว่าคุณจะไม่ได้ยกตัวอย่างมันเพราะถ้ารู้ sizeof (U) อยู่เสมอ> = 1 และ ...
โยฮันเน Schaub - litb

20

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


กฎทั่วไปสำหรับการวางtypenameคำหลักนั้นส่วนใหญ่เมื่อคุณใช้พารามิเตอร์เทมเพลตและคุณต้องการเข้าถึงซ้อนtypedefหรือใช้นามแฝง:

template<typename T>
struct test {
    using type = T; // no typename required
    using underlying_type = typename T::type // typename required
};

โปรดทราบว่าสิ่งนี้ยังใช้กับฟังก์ชั่นเมตาหรือสิ่งที่ใช้พารามิเตอร์แม่แบบทั่วไปด้วย อย่างไรก็ตามหากพารามิเตอร์เทมเพลตที่ให้ไว้เป็นประเภทที่ชัดเจนคุณไม่จำเป็นต้องระบุtypenameตัวอย่างเช่น:

template<typename T>
struct test {
    // typename required
    using type = typename std::conditional<true, const T&, T&&>::type;
    // no typename required
    using integer = std::conditional<true, int, float>::type;
};

กฎทั่วไปสำหรับการเพิ่มตัวtemplateคัดเลือกส่วนใหญ่จะคล้ายกันยกเว้นพวกเขามักจะเกี่ยวข้องกับฟังก์ชั่นสมาชิก templated (คงที่หรืออย่างอื่น) ของ struct / class ที่ templated ตัวเองเช่น:

รับ struct นี้และฟังก์ชั่น:

template<typename T>
struct test {
    template<typename U>
    void get() const {
        std::cout << "get\n";
    }
};

template<typename T>
void func(const test<T>& t) {
    t.get<int>(); // error
}

การพยายามเข้าถึงt.get<int>()จากภายในฟังก์ชันจะทำให้เกิดข้อผิดพลาด:

main.cpp:13:11: error: expected primary-expression before 'int'
     t.get<int>();
           ^
main.cpp:13:11: error: expected ';' before 'int'

ดังนั้นในบริบทนี้คุณจะต้องใช้templateคำหลักล่วงหน้าและเรียกมันว่า:

t.template get<int>()

t.get < intวิธีการที่คอมไพเลอร์จะแยกนี้ถูกต้องมากกว่า


2

ฉันกำลังตอบสนองอย่างดีเยี่ยมของ JLBorges ต่อคำถามที่เหมือนกันทุกประการจาก cplusplus.com เนื่องจากเป็นคำอธิบายที่กระชับที่สุดที่ฉันได้อ่านในหัวข้อนี้

ในเทมเพลตที่เราเขียนมีชื่อสองชนิดที่สามารถใช้ - ชื่อที่ต้องพึ่งพาและชื่อที่ไม่ขึ้นอยู่กับ ชื่อที่ขึ้นต่อกันคือชื่อที่ขึ้นอยู่กับพารามิเตอร์เทมเพลต ชื่อที่ไม่ขึ้นอยู่กับมีความหมายเหมือนกันไม่ว่าพารามิเตอร์เทมเพลตคืออะไร

ตัวอย่างเช่น:

template< typename T > void foo( T& x, std::string str, int count )
{
    // these names are looked up during the second phase
    // when foo is instantiated and the type T is known
    x.size(); // dependant name (non-type)
    T::instance_count ; // dependant name (non-type)
    typename T::iterator i ; // dependant name (type)

    // during the first phase, 
    // T::instance_count is treated as a non-type (this is the default)
    // the typename keyword specifies that T::iterator is to be treated as a type.

    // these names are looked up during the first phase
    std::string::size_type s ; // non-dependant name (type)
    std::string::npos ; // non-dependant name (non-type)
    str.empty() ; // non-dependant name (non-type)
    count ; // non-dependant name (non-type)
}

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

ในระหว่างเฟสแรกตัวแยกวิเคราะห์จำเป็นต้องรู้ว่าชื่อที่ขึ้นต่อกันนั้นเป็นชื่อของชนิดหรือชื่อที่ไม่ใช่ประเภท โดยค่าเริ่มต้นชื่อที่อ้างอิงจะถือว่าเป็นชื่อที่ไม่ใช่ประเภท คีย์เวิร์ด typename ก่อนชื่อที่ขึ้นต่อกันระบุว่าเป็นชื่อของชนิด


สรุป

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

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