อย่างเป็นทางการชื่อประเภทมีไว้เพื่ออะไร?


131

ในบางครั้งฉันเคยเห็นข้อความแสดงข้อผิดพลาดที่อ่านไม่ออกจริงๆบางอย่างพ่นออกมาgccเมื่อใช้เทมเพลต ... โดยเฉพาะอย่างยิ่งฉันมีปัญหาที่การประกาศที่ดูเหมือนถูกต้องทำให้เกิดข้อผิดพลาดในการคอมไพล์ที่แปลกประหลาดซึ่งหายไปอย่างน่าอัศจรรย์โดยใส่typenameคำหลักไว้ที่จุดเริ่มต้นของ การประกาศ ... (ตัวอย่างเช่นเมื่อสัปดาห์ที่แล้วฉันประกาศตัววนซ้ำสองตัวเป็นสมาชิกของคลาสเทมเพลตอื่นและฉันต้องทำสิ่งนี้) ...

เรื่องราวเกี่ยวกับtypenameอะไร?


คำตอบ:


207

ต่อไปนี้เป็นคำพูดจากหนังสือ Josuttis:

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

template <class T>
Class MyClass
{
  typename T::SubType * ptr;
  ...
};

ที่นี่typenameใช้เพื่อชี้แจงว่า SubTypeเป็นประเภทของclass T. ดังนั้นจึง เป็นตัวชี้ไปยังชนิดptr T::SubTypeโดยไม่typename, SubType จะได้รับการพิจารณาเป็นสมาชิกแบบคงที่ ดังนั้น

T::SubType * ptr

จะเป็นคูณของค่า SubTypeประเภทด้วยTptr


2
หนังสือที่ดี อ่านครั้งเดียวแล้วเก็บไว้เป็นข้อมูลอ้างอิงหากคุณต้องการ
deft_code

1
ผู้อ่านที่ชาญฉลาดจะทราบว่าไม่อนุญาตให้ใช้นิพจน์การคูณโดยไวยากรณ์สำหรับการประกาศสมาชิก ดังนั้น C ++ 20 จึงไม่จำเป็นต้องใช้สิ่งนี้typename(แม้ว่าจะไม่ใช่ทั้งหมด!)
Davis Herring

ไม่ได้โน้มน้าวฉัน. เมื่อเทมเพลตถูกสร้างอินสแตนซ์แล้วจะมีการกำหนดเป็นอย่างดีว่า T :: Subtype คืออะไร
kovarex

36

โพสต์ BLog ของ Stan Lippmanแนะนำ: -

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

โดยทั่วไปแล้ว Stroustrup ใช้คีย์เวิร์ดคลาสซ้ำโดยไม่แนะนำคีย์เวิร์ดใหม่ซึ่งมีการเปลี่ยนแปลงในภายหลังในมาตรฐานด้วยเหตุผลต่อไปนี้

ดังตัวอย่างที่กำหนด

template <class T>
class Demonstration {
public:
void method() {
    T::A *aObj; // oops …
     // …
};

ไวยากรณ์ของภาษาแปลผิดT::A *aObj;เป็นนิพจน์ทางคณิตศาสตร์ดังนั้นจึงมีการแนะนำคำหลักใหม่ที่เรียกว่าtypename

typename T::A* a6;

มันสั่งให้คอมไพลเลอร์ปฏิบัติต่อคำสั่งที่ตามมาเป็นการประกาศ

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

นั่นคือเหตุผลที่เรามีทั้งสองอย่าง

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


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

5
@ เจสเปอร์: ฉันคิดว่าคำตอบของ Xenus ทำให้สับสนที่นี่ typenameจำเป็นต้องแก้ไขปัญหาการแยกวิเคราะห์ตามที่อธิบายไว้ในคำตอบของ Naveen โดยอ้างถึง Josuttis (ฉันไม่คิดว่าการแทรก a classในสถานที่นี้จะได้ผล) หลังจากยอมรับคำหลักใหม่สำหรับกรณีนี้เท่านั้นคำหลักนี้ยังได้รับอนุญาตในการประกาศอาร์กิวเมนต์เทมเพลต ( หรือคำจำกัดความนั้น? ) เนื่องจากclassมีอยู่เสมอ ทำให้เข้าใจผิด
sbi

13

พิจารณารหัส

template<class T> somefunction( T * arg )
{
    T::sometype x; // broken
    .
    .

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

template<class T> somefunction( T * arg )
{
    typename T::sometype x; // works!
    .
    .

6

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

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

template <class T> struct S {
  typename T::type i;
};

ในตัวอย่างนี้คีย์เวิร์ดที่typenameจำเป็นสำหรับโค้ดในการคอมไพล์

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

template <class T> struct S {
  T::template ptr<int> p;
};

ในบางกรณีอาจจำเป็นต้องใช้ทั้งสองอย่าง

template <class T> struct S {
  typename T::template ptr<int>::type i;
};

(ถ้าฉันมีไวยากรณ์ถูกต้อง)

แน่นอนบทบาทอื่นของคีย์เวิร์ดtypenameคือการใช้ในการประกาศพารามิเตอร์เทมเพลต


ดูคำอธิบายของคีย์เวิร์ดประเภท C ++สำหรับข้อมูลเพิ่มเติม (พื้นหลัง)
Atafar

5

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

template<typename T>
struct test {
    typedef T* ptr;
};

template<>         // complete specialization 
struct test<int> { // for the case T is int
    T* ptr;
};

อาจมีคนถามว่าทำไมสิ่งนี้ถึงมีประโยชน์และแท้จริงแล้วมันดูไร้ประโยชน์จริงๆ แต่ใช้เวลาในใจว่าตัวอย่างเช่นประเภทมีลักษณะที่แตกต่างกันอย่างสิ้นเชิงกว่าอื่น ๆs เป็นที่ยอมรับว่ามันไม่ได้เปลี่ยนรูปแบบจากประเภทไปสู่สิ่งที่แตกต่างออกไป แต่อย่างไรก็ตามมันอาจเกิดขึ้นได้std::vector<bool>referenceTreference

จะเกิดอะไรขึ้นถ้าคุณเขียนเทมเพลตของคุณเองโดยใช้testเทมเพลตนี้ อะไรทำนองนี้

template<typename T>
void print(T& x) {
    test<T>::ptr p = &x;
    std::cout << *p << std::endl;
}

ดูเหมือนว่าจะโอเคสำหรับคุณเพราะคุณคาดหวังว่าtest<T>::ptrจะเป็นประเภท แต่คอมไพเลอร์ไม่ทราบและในการกระทำเขาได้รับคำแนะนำจากมาตรฐานให้คาดหวังสิ่งที่ตรงกันข้ามtest<T>::ptrไม่ใช่ประเภท เพื่อบอกคอมไพเลอร์ถึงสิ่งที่คุณคาดหวังว่าคุณต้องเพิ่มtypenameก่อนหน้านี้ เทมเพลตที่ถูกต้องมีลักษณะดังนี้

template<typename T>
void print(T& x) {
    typename test<T>::ptr p = &x;
    std::cout << *p << std::endl;
}

บรรทัดล่าง: คุณต้องเพิ่มtypenameก่อนเมื่อใดก็ตามที่คุณใช้แม่แบบที่ซ้อนกันในเทมเพลตของคุณ (แน่นอนเฉพาะในกรณีที่พารามิเตอร์เทมเพลตของเทมเพลตของคุณถูกใช้สำหรับเทมเพลตภายในนั้น)


5

ใช้สองอย่าง:

  1. เป็นtemplateคีย์เวิร์ดของอาร์กิวเมนต์ (แทนclass)
  2. typenameคำหลักบอกคอมไพเลอร์ที่ระบุเป็นชนิด (มากกว่าตัวแปรสมาชิกคงที่)
template <typename T> class X  // [1]
{
    typename T::Y _member;  // [2] 
}

4

ฉันคิดว่าคำตอบทั้งหมดได้กล่าวถึงtypenameคำหลักนั้นใช้ในสองกรณีที่แตกต่างกัน:

a) เมื่อประกาศพารามิเตอร์ประเภทเทมเพลต เช่น

template<class T> class MyClass{};        // these two cases are
template<typename T> class MyNewClass{};  // exactly the same.

ซึ่งไม่มีความแตกต่างระหว่างพวกเขาและพวกเขาเหมือนกันอย่างแน่นอน

b) ก่อนที่จะใช้ชื่อประเภทที่ขึ้นกับซ้อนกันสำหรับเทมเพลต

template<class T>
void foo(const T & param)
{
   typename T::NestedType * value; // we should use typename here
}

ซึ่งการไม่ใช้typenameโอกาสในการขายทำให้เกิดข้อผิดพลาดในการแยกวิเคราะห์ / คอมไพล์

สิ่งที่ฉันต้องการที่จะเพิ่มในกรณีที่สองเป็นที่กล่าวถึงในหนังสือสกอตเมเยอร์ที่มีประสิทธิภาพ C ++ , คือว่ามีข้อยกเว้นของการใช้typenameมาก่อนชื่อประเภทที่ซ้อนกันขึ้นอยู่กับ ข้อยกเว้นคือถ้าคุณใช้ชื่อชนิดอ้างอิงที่ซ้อนกันเป็น aคลาสฐานหรือในรายการการเริ่มต้นสมาชิกคุณไม่ควรใช้ที่typenameนั่น:

template<class T>
class D : public B<T>::NestedType               // No need for typename here
{
public:
   D(std::string str) : B<T>::NestedType(str)   // No need for typename here
   {
      typename B<T>::AnotherNestedType * x;     // typename is needed here
   }
}

หมายเหตุ:การใช้typenameไม่จำเป็นต้องสำหรับกรณีที่สอง (เช่นก่อนชื่อชนิดที่ขึ้นกับซ้อนกัน) เนื่องจาก C ++ 20


2
#include <iostream>

class A {
public:
    typedef int my_t;
};

template <class T>
class B {
public:
    // T::my_t *ptr; // It will produce compilation error
    typename T::my_t *ptr; // It will output 5
};

int main() {
    B<A> b;
    int my_int = 5;
    b.ptr = &my_int;
    std::cout << *b.ptr;
    std::cin.ignore();
    return 0;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.