Tôi phải đặt từ khóa kiểu mẫu ở đâu


1126

Trong các mẫu, tôi phải đặt typenamevà đặt templatetên phụ thuộc ở đâu và tại sao ?
Chính xác thì tên phụ thuộc là gì?

Tôi có đoạn mã sau:

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> {
    };
};

Vấn đề tôi có là trong typedef Tail::inUnion<U> dummydòng. Tôi khá chắc chắn đó inUnionlà một tên phụ thuộc và VC ++ hoàn toàn đúng khi bóp nghẹt nó.
Tôi cũng biết rằng tôi sẽ có thể thêm vào templatemột nơi nào đó để nói với trình biên dịch rằng inUnion là một id mẫu. Nhưng chính xác là ở đâu? Và sau đó có nên giả sử rằng inUnion là một mẫu lớp, tức là inUnion<U>đặt tên một loại chứ không phải là một hàm?


1
Câu hỏi gây phiền nhiễu: tại sao không tăng :: Biến thể?
Assaf Lavie

58
Nhạy cảm chính trị, tính di động.
MSalters

5
Tôi đã làm cho câu hỏi thực tế của bạn ("Đặt mẫu / tên chữ ở đâu?") Nổi bật hơn bằng cách đặt câu hỏi và mã cuối cùng ở đầu và rút ngắn mã theo chiều ngang để vừa với màn hình 1024x.
Julian Schaub - litb

7
Đã xóa "tên phụ thuộc" khỏi tiêu đề vì có vẻ như hầu hết những người thắc mắc về "tên chữ" và "mẫu" không biết "tên phụ thuộc" là gì. Nó sẽ ít gây nhầm lẫn cho họ theo cách này.
Julian Schaub - litb

2
@MSalters: boost khá di động. Tôi chỉ nói rằng chính trị là lý do chung tại sao việc tăng cường thường không được chú ý. Lý do tốt duy nhất tôi biết là thời gian xây dựng tăng lên. Nếu không, đây là tất cả về việc mất hàng ngàn đô la phát minh lại bánh xe.
v.oddou

Câu trả lời:


1164

(Xem thêm ở đây để biết câu trả lời C ++ 11 của tôi )

Để phân tích một chương trình C ++, trình biên dịch cần biết một số tên nhất định có phải là loại hay không. Ví dụ sau đây chứng minh rằng:

t * f;

Làm thế nào điều này nên được phân tích cú pháp? Đối với nhiều ngôn ngữ, trình biên dịch không cần biết ý nghĩa của tên để phân tích cú pháp và về cơ bản biết hành động của một dòng mã. Trong C ++, tuy nhiên, ở trên có thể mang lại nhiều cách hiểu khác nhau tùy thuộc vào tphương tiện. Nếu đó là một loại, thì nó sẽ là một khai báo của một con trỏ f. Tuy nhiên, nếu nó không phải là một loại, nó sẽ là một phép nhân. Vì vậy, Tiêu chuẩn C ++ nói tại đoạn (3/7):

Một số tên biểu thị các loại hoặc mẫu. Nói chung, bất cứ khi nào một tên gặp phải, cần phải xác định xem tên đó biểu thị một trong những thực thể này trước khi tiếp tục phân tích chương trình có chứa nó. Quá trình xác định điều này được gọi là tra cứu tên.

Làm thế nào trình biên dịch sẽ tìm ra những gì một tên t::xđề cập đến, nếu tđề cập đến một tham số loại mẫu? xcó thể là một thành viên dữ liệu int tĩnh có thể được nhân lên hoặc cũng có thể là một lớp lồng nhau hoặc typedef có thể mang lại một khai báo. Nếu một tên có thuộc tính này - rằng nó không thể được tra cứu cho đến khi biết các đối số mẫu thực tế - thì nó được gọi là tên phụ thuộc (nó "phụ thuộc" vào các tham số mẫu).

Bạn có thể khuyên bạn chỉ nên đợi cho đến khi người dùng khởi tạo mẫu:

Chúng ta hãy đợi cho đến khi người dùng khởi tạo mẫu và sau đó tìm hiểu ý nghĩa thực sự của nó t::x * f;.

Điều này sẽ hoạt động và thực sự được Tiêu chuẩn cho phép như một cách tiếp cận thực hiện có thể. Các trình biên dịch này về cơ bản sao chép văn bản của mẫu vào bộ đệm bên trong và chỉ khi cần khởi tạo, chúng mới phân tích mẫu và có thể phát hiện lỗi trong định nghĩa. Nhưng thay vì làm phiền người dùng của mẫu (đồng nghiệp nghèo!) Với các lỗi do tác giả của mẫu tạo ra, các triển khai khác chọn kiểm tra mẫu sớm và đưa ra lỗi trong định nghĩa càng sớm càng tốt, trước khi việc khởi tạo thậm chí diễn ra.

Vì vậy, phải có một cách để nói với trình biên dịch rằng một số tên nhất định là các loại và các tên nhất định không có.

Từ khóa "typename"

Câu trả lời là: Chúng tôi quyết định cách trình biên dịch nên phân tích cú pháp này. Nếu t::xlà một tên phụ thuộc, thì chúng ta cần phải thêm tiền tố vào nó typenameđể báo cho trình biên dịch phân tích cú pháp theo một cách nhất định. Tiêu chuẩn cho biết tại (14.6 / 2):

Tên được sử dụng trong khai báo hoặc định nghĩa mẫu và phụ thuộc vào tham số mẫu được giả sử không đặt tên loại trừ khi tra cứu tên áp dụng tìm thấy tên loại hoặc tên đủ điều kiện theo tên kiểu từ khóa.

Có nhiều tên typenamekhông cần thiết, bởi vì trình biên dịch có thể, với tra cứu tên có thể áp dụng trong định nghĩa mẫu, tìm ra cách phân tích một cấu trúc - ví dụ như T *f;, khi nào Tlà tham số mẫu kiểu. Nhưng t::x * f;để trở thành một tuyên bố, nó phải được viết như typename t::x *f;. Nếu bạn bỏ qua từ khóa và tên được coi là không phải loại, nhưng khi khởi tạo tìm thấy nó biểu thị một loại, các thông báo lỗi thông thường được trình biên dịch phát ra. Đôi khi, lỗi do đó được đưa ra tại thời điểm định nghĩa:

// 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;

Cú pháp typenamechỉ cho phép trước các tên đủ điều kiện - do đó được coi là các tên không đủ tiêu chuẩn luôn được biết là tham chiếu đến các loại nếu chúng làm như vậy.

Một gotcha tương tự tồn tại cho các tên biểu thị các mẫu, như được gợi ý bởi văn bản giới thiệu.

Từ khóa "mẫu"

Ghi nhớ trích dẫn ban đầu ở trên và làm thế nào Tiêu chuẩn yêu cầu xử lý đặc biệt cho các mẫu? Hãy lấy ví dụ trông ngây thơ sau đây:

boost::function< int() > f;

Nó có thể trông rõ ràng đối với một người đọc con người. Không phải như vậy cho trình biên dịch. Hãy tưởng tượng định nghĩa tùy ý sau đây về boost::functionf:

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

Đó thực sự là một biểu thức hợp lệ ! Nó sử dụng toán tử nhỏ hơn để so sánh boost::functionvới zero ( int()) và sau đó sử dụng toán tử lớn hơn để so sánh kết quả boolvới f. Tuy nhiên, như bạn có thể biết, boost::function trong cuộc sống thực là một khuôn mẫu, vì vậy trình biên dịch biết (14.2 / 3):

Sau khi tra cứu tên (3.4) thấy rằng tên là tên mẫu, nếu tên này được theo sau bởi <, <luôn được lấy làm phần đầu của danh sách đối số mẫu và không bao giờ là tên được theo sau bởi ít hơn hơn nhà điều hành.

Bây giờ chúng tôi trở lại vấn đề tương tự như với typename. Điều gì xảy ra nếu chúng ta chưa biết liệu tên có phải là mẫu khi phân tích mã không? Chúng tôi sẽ cần phải chèn templatengay trước tên mẫu, như được chỉ định bởi 14.2/4. Điều này trông giống như:

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

Tên mẫu không chỉ có thể xảy ra sau một ::nhưng cũng sau một ->hoặc .trong một truy cập thành viên lớp. Bạn cũng cần chèn từ khóa vào đó:

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

Phụ thuộc

Đối với những người có sách Standardese dày trên kệ của họ và muốn biết chính xác những gì tôi đang nói, tôi sẽ nói một chút về cách thức này được quy định trong Tiêu chuẩn.

Trong khai báo mẫu, một số cấu trúc có ý nghĩa khác nhau tùy thuộc vào đối số mẫu bạn sử dụng để khởi tạo mẫu: Biểu thức có thể có các loại hoặc giá trị khác nhau, các biến có thể có các kiểu gọi hoặc hàm khác nhau có thể gọi các hàm khác nhau. Các cấu trúc như vậy thường được cho là phụ thuộc vào các tham số mẫu.

Tiêu chuẩn xác định chính xác các quy tắc bằng cách xây dựng có phụ thuộc hay không. Nó phân tách chúng thành các nhóm khác nhau về mặt logic: Một loại bắt các loại, một loại khác bắt các biểu thức. Biểu thức có thể phụ thuộc bởi giá trị của chúng và / hoặc loại của chúng. Vì vậy, chúng tôi có, với các ví dụ điển hình được nối thêm:

  • Các loại phụ thuộc (ví dụ: tham số mẫu loại T)
  • Biểu thức phụ thuộc giá trị (ví dụ: tham số mẫu không phải kiểu N)
  • Các biểu thức phụ thuộc vào loại (ví dụ: truyền vào tham số mẫu loại (T)0)

Hầu hết các quy tắc là trực quan và được xây dựng đệ quy: Ví dụ: một loại được xây dựng dưới T[N]dạng loại phụ thuộc nếu Nlà biểu thức phụ thuộc giá trị hoặc Tlà loại phụ thuộc. Các chi tiết của điều này có thể được đọc trong phần (14.6.2/1) cho các loại phụ thuộc, (14.6.2.2)cho các biểu thức phụ thuộc loại và (14.6.2.3)cho các biểu thức phụ thuộc giá trị.

Tên người phụ thuộc

Tiêu chuẩn là một chút không rõ ràng về những gì chính xác là một tên phụ thuộc . Trong một lần đọc đơn giản (bạn biết, nguyên tắc ít bất ngờ nhất), tất cả những gì nó định nghĩa là một tên phụ thuộc là trường hợp đặc biệt cho các tên hàm bên dưới. Nhưng vì rõ ràng T::xcũng cần phải được tra cứu trong bối cảnh khởi tạo, nên nó cũng cần phải là một tên phụ thuộc (may mắn thay, kể từ giữa C ++ 14, ủy ban đã bắt đầu xem xét cách khắc phục định nghĩa khó hiểu này).

Để tránh vấn đề này, tôi đã sử dụng một cách giải thích đơn giản về văn bản Tiêu chuẩn. Trong tất cả các cấu trúc biểu thị các loại hoặc biểu thức phụ thuộc, một tập hợp con của chúng đại diện cho các tên. Những cái tên đó là "tên phụ thuộc". Một tên có thể có các hình thức khác nhau - Tiêu chuẩn nói:

Tên là việc sử dụng mã định danh (2.11), toán tử-hàm-id (13,5), hàm chuyển đổi-id (12.3.2) hoặc id-id (14.2) biểu thị một thực thể hoặc nhãn (6.6.4, 6.1)

Một định danh chỉ là một chuỗi ký tự / chữ số đơn giản, trong khi hai ký tự tiếp theo là operator +operator typebiểu mẫu. Hình thức cuối cùng là template-name <argument list>. Tất cả đều là tên và bằng cách sử dụng thông thường trong Tiêu chuẩn, một tên cũng có thể bao gồm các vòng loại cho biết không gian tên hoặc lớp nào mà tên cần được tra cứu.

Một biểu thức phụ thuộc giá trị 1 + Nkhông phải là một tên, nhưng Nlà. Tập hợp con của tất cả các cấu trúc phụ thuộc là các tên được gọi là tên phụ thuộc . Tuy nhiên, tên hàm có thể có ý nghĩa khác nhau trong các lần xuất hiện khác nhau của mẫu, nhưng không may là không bị quy tắc chung này bắt.

Tên hàm phụ thuộc

Không phải chủ yếu là một mối quan tâm của bài viết này, nhưng vẫn đáng được đề cập: Tên hàm là một ngoại lệ được xử lý riêng. Tên hàm định danh phụ thuộc không phải bởi chính nó, mà bởi các biểu thức đối số phụ thuộc loại được sử dụng trong một cuộc gọi. Trong ví dụ f((T)0), flà một tên phụ thuộc. Trong Tiêu chuẩn, điều này được chỉ định tại (14.6.2/1).

Ghi chú và ví dụ bổ sung

Trong trường hợp đủ, chúng ta cần cả typenametemplate. Mã của bạn sẽ trông như sau

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

Từ khóa templatekhông phải luôn luôn xuất hiện trong phần cuối của tên. Nó có thể xuất hiện ở giữa trước một tên lớp được sử dụng làm phạm vi, như trong ví dụ sau

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

Trong một số trường hợp, các từ khóa bị cấm, như chi tiết dưới đây

  • Về tên của một lớp cơ sở phụ thuộc, bạn không được phép viết typename. Nó giả định rằng tên được đưa ra là một loại tên lớp. Điều này đúng cho cả hai tên trong danh sách lớp cơ sở và danh sách trình khởi tạo hàm tạo:

     template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
  • Trong các khai báo sử dụng, không thể sử dụng templatesau lần cuối ::và ủy ban C ++ nói không hoạt động trên một giải pháp.

     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
Câu trả lời này đã được sao chép từ mục Câu hỏi thường gặp trước đây của tôi mà tôi đã xóa, vì tôi thấy rằng tôi nên sử dụng tốt hơn các câu hỏi tương tự hiện có thay vì tạo ra các "câu hỏi giả" mới chỉ nhằm mục đích trả lời chúng. Cảm ơn @Prasoon , người đã chỉnh sửa ý tưởng của phần cuối cùng (trường hợp cấm tên / mẫu) vào câu trả lời.
Julian Schaub - litb

1
Bạn có thể giúp tôi khi nào tôi nên sử dụng cú pháp này? này-> mẫu f <int> (); Tôi nhận được lỗi này 'mẫu' (với tư cách là người định hướng) chỉ được phép trong các mẫu nhưng không có từ khóa mẫu, nó hoạt động tốt.
balki

1
Tôi đã hỏi một câu hỏi tương tự ngày hôm nay, nó đã sớm được đánh dấu là trùng lặp: stackoverflow.com/questions/27923722/ . Tôi đã được hướng dẫn để làm sống lại câu hỏi này thay vì tạo một câu hỏi mới. Tôi phải nói rằng tôi không đồng ý rằng chúng là bản sao nhưng tôi là ai, phải không? Vì vậy, có bất kỳ lý do nào typenameđược thi hành ngay cả khi cú pháp cho phép không có cách hiểu khác ngoài tên loại tại thời điểm này?
JorenHeit

1
@Pablo bạn không thiếu thứ gì. Nhưng vẫn được yêu cầu viết định hướng ngay cả khi dòng hoàn chỉnh sẽ không còn mơ hồ.
Julian Schaub - litb

1
@Pablo mục đích là để giữ cho ngôn ngữ và trình biên dịch đơn giản hơn. Có những đề xuất cho phép nhiều tình huống tự động tìm ra mọi thứ, do đó bạn cần từ khóa ít thường xuyên hơn. Lưu ý rằng trong ví dụ của bạn, token mơ hồ và chỉ sau khi bạn đã thấy ">" sau khi tăng gấp đôi, bạn có thể disambiguate nó như là một khung mẫu góc. Để biết thêm chi tiết, tôi là người hỏi sai, vì tôi không có kinh nghiệm trong việc triển khai trình phân tích cú pháp của trình biên dịch C ++.
Julian Schaub - litb

136

C ++ 11

Vấn đề

Mặc dù các quy tắc trong C ++ 03 về thời điểm bạn cần typenametemplatephần lớn là hợp lý, có một nhược điểm khó chịu trong công thức của nó

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();
};

Như có thể thấy, chúng ta cần từ khóa định hướng ngay cả khi trình biên dịch hoàn toàn có thể tự tìm ra chính nó A::result_typechỉ có thể int(và do đó là một loại) và this->gchỉ có thể là mẫu thành viên được gkhai báo sau (ngay cả khi Ađược chuyên môn hóa ở đâu đó, điều đó sẽ không ảnh hưởng đến mã trong mẫu đó, vì vậy ý ​​nghĩa của nó không thể bị ảnh hưởng bởi sự chuyên môn hóa sau này A!).

Khởi tạo hiện tại

Để cải thiện tình hình, trong C ++ 11, ngôn ngữ theo dõi khi một loại tham chiếu đến mẫu kèm theo. Để biết rằng, loại phải được hình thành bằng cách sử dụng một hình thức nhất định về tên, đó là tên riêng của mình (ở trên, A, A<T>, ::A<T>). Một loại được tham chiếu bởi một tên như vậy được gọi là khởi tạo hiện tại . Có thể có nhiều loại là tất cả các khởi tạo hiện tại nếu loại mà tên được hình thành là một lớp thành viên / lồng nhau (sau đó, A::NestedClassAcả hai tức thời hiện tại).

Dựa trên khái niệm này, ngôn ngữ nói rằng CurrentInstantiation::Foo, FooCurrentInstantiationTyped->Foo(chẳng hạn như A *a = this; a->Foo) đều là thành viên của khởi tạo hiện tại nếu chúng được tìm thấy là thành viên của một lớp là khởi tạo hiện tại hoặc một trong các lớp cơ sở không phụ thuộc của nó (chỉ bằng cách thực hiện Tên tra cứu ngay lập tức).

Các từ khóa typenametemplatebây giờ không còn cần thiết nữa nếu vòng loại là thành viên của khởi tạo hiện tại. Một keypoint đây để ghi nhớ là A<T>vẫn một tên loại phụ thuộc (sau khi tất cả Tcũng được gõ phụ thuộc). Nhưng A<T>::result_typeđược biết đến là một loại - trình biên dịch sẽ "kỳ diệu" xem xét loại loại phụ thuộc này để tìm ra loại này.

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;        
  }
};

Điều đó thật ấn tượng, nhưng chúng ta có thể làm tốt hơn không? Ngôn ngữ thậm chí còn đi xa hơn và yêu cầu một triển khai lại tìm kiếm D::result_typekhi khởi tạo D::f(ngay cả khi nó đã tìm thấy ý nghĩa của nó tại thời điểm định nghĩa). Khi bây giờ kết quả tra cứu khác nhau hoặc dẫn đến sự mơ hồ, chương trình không được định dạng và phải đưa ra chẩn đoán. Hãy tưởng tượng điều gì xảy ra nếu chúng ta định nghĩa Cnhư thế này

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

Một trình biên dịch là cần thiết để bắt lỗi khi khởi tạo D<int>::f. Vì vậy, bạn có được điều tốt nhất trong hai thế giới: Tra cứu "Trì hoãn" bảo vệ bạn nếu bạn có thể gặp rắc rối với các lớp cơ sở phụ thuộc và tra cứu "Ngay lập tức" giải phóng bạn khỏi typenametemplate.

Chuyên ngành chưa biết

Trong mã của D, tên typename D::questionable_typekhông phải là thành viên của khởi tạo hiện tại. Thay vào đó, ngôn ngữ đánh dấu nó là một thành viên của một chuyên ngành chưa biết . Cụ thể, đây luôn là trường hợp khi bạn đang thực hiện DependentTypeName::Foohoặc hoặc DependentTypedName->Fooloại phụ thuộc không phải là khởi tạo hiện tại (trong trường hợp đó trình biên dịch có thể từ bỏ và nói "chúng ta sẽ xem xét lại sau Foo) hoặc đó khởi tạo hiện tại và Tên không được tìm thấy trong nó hoặc các lớp cơ sở không phụ thuộc của nó và cũng có các lớp cơ sở phụ thuộc.

Hãy tưởng tượng điều gì xảy ra nếu chúng ta có một hàm thành viên htrong Akhuôn mẫu lớp được xác định ở trên

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

Trong C ++ 03, ngôn ngữ được phép bắt lỗi này vì không bao giờ có thể có một cách hợp lệ để khởi tạo A<T>::h(bất kỳ đối số nào bạn đưa ra T). Trong C ++ 11, ngôn ngữ hiện có thêm một kiểm tra để đưa ra thêm lý do cho trình biên dịch thực hiện quy tắc này. Vì Akhông có các lớp cơ sở phụ thuộc và Atuyên bố không có thành viên questionable_type, nên tên A<T>::questionable_typenày không phải là thành viên của khởi tạo hiện tại cũng nhưmột thành viên của một chuyên ngành chưa biết. Trong trường hợp đó, không có cách nào mà mã đó có thể biên dịch hợp lệ tại thời điểm khởi tạo, vì vậy ngôn ngữ cấm một tên trong đó vòng loại hiện tại không phải là thành viên của một chuyên ngành không xác định cũng không phải là thành viên của khởi tạo hiện tại (tuy nhiên , vi phạm này vẫn không bắt buộc phải được chẩn đoán).

Ví dụ và câu đố

Bạn có thể thử kiến ​​thức này về câu trả lời này và xem liệu các định nghĩa trên có hợp lý với bạn trong một ví dụ thực tế hay không (chúng được lặp lại ít chi tiết hơn trong câu trả lời đó).

Các quy tắc C ++ 11 làm cho mã C ++ 03 hợp lệ sau đây không được định dạng (không được ủy ban C ++ dự định, nhưng có lẽ sẽ không được sửa)

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(); 
}

Mã C ++ 03 hợp lệ này sẽ liên kết this->fvới A::ftại thời điểm khởi tạo và mọi thứ đều ổn. Tuy nhiên, C ++ 11 liên kết ngay lập tức B::fvà yêu cầu kiểm tra kỹ khi khởi tạo, kiểm tra xem tra cứu có còn phù hợp hay không. Tuy nhiên, khi khởi tạo C<A>::g, Quy tắc thống trị được áp dụng và tra cứu sẽ tìm thấy A::fthay thế.


fyi - câu trả lời này được tham khảo tại đây: stackoverflow.com/questions/56411114/ Khăn Phần lớn mã trong câu trả lời này không được biên dịch trên các trình biên dịch khác nhau.
Adam Rackis

@AdamRackis giả sử rằng thông số C ++ không thay đổi kể từ năm 2013 (ngày mà tôi đã viết câu trả lời này), sau đó các trình biên dịch mà bạn đã thử mã của mình chỉ đơn giản là chưa thực hiện tính năng C ++ 11 + này.
Julian Schaub - litb

100

TRƯỚC

Bài đăng này có nghĩa là một thay thế dễ đọc cho bài viết của litb .

Mục đích cơ bản là như nhau; một lời giải thích cho "Khi nào?" và tại sao?" typenametemplatephải được áp dụng.


Mục đích của typenamevà là templategì?

typenametemplatecó thể sử dụng trong các trường hợp khác hơn là khi khai báo mẫu.

Có một số bối cảnh nhất định trong C ++ , trong đó trình biên dịch phải được nói rõ ràng về cách xử lý tên và tất cả các bối cảnh này đều có một điểm chung; chúng phụ thuộc vào ít nhất một tham số mẫu .

Chúng tôi đề cập đến những cái tên như vậy, nơi có thể có một sự mơ hồ trong việc giải thích, như; " Tên phụ thuộc ".

Bài đăng này sẽ đưa ra lời giải thích cho mối quan hệ giữa tên phụ thuộc và hai từ khóa.


MỘT SNIPPET NÓI HƠN 1000 WORDS

Cố gắng giải thích những gì đang diễn ra trong mẫu chức năng sau đây , cho chính bạn, một người bạn, hoặc có lẽ là con mèo của bạn; những gì đang xảy ra trong tuyên bố được đánh dấu ( A )?

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


Nó có thể không dễ như người ta nghĩ, cụ thể hơn là kết quả của việc đánh giá ( A ) phụ thuộc rất nhiều vào định nghĩa của loại được truyền dưới dạng tham số mẫu T.

Các Ts khác nhau có thể thay đổi mạnh mẽ các ngữ nghĩa liên quan.

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


Hai kịch bản khác nhau :

  • Nếu chúng ta khởi tạo khuôn mẫu hàm với kiểu X , như trong ( C ), chúng ta sẽ có một khai báo của một con trỏ - đến int có tên x , nhưng;

  • nếu chúng ta khởi tạo mẫu bằng loại Y , như trong ( D ), ( A ) thay vào đó sẽ bao gồm một biểu thức tính tích của 123 nhân với một số biến x đã được khai báo .



CƠ SỞ LÝ LUẬN

Tiêu chuẩn C ++ quan tâm đến sự an toàn và sức khỏe của chúng tôi, ít nhất là trong trường hợp này.

Để ngăn việc triển khai có khả năng chịu những bất ngờ khó chịu, Tiêu chuẩn yêu cầu chúng tôi loại bỏ sự mơ hồ của tên phụ thuộc bằng cách nêu rõ ý định ở bất cứ nơi nào chúng tôi muốn coi tên đó là tên loại hoặc mẫu- id .

Nếu không có gì được nêu, tên phụ thuộc sẽ được coi là một biến hoặc một hàm.



LÀM THẾ NÀO ĐỂ XỬ LÝ TÊN NỮA ?

Nếu đây là một bộ phim Hollywood, tên phụ thuộc sẽ là căn bệnh lây lan qua tiếp xúc cơ thể, ngay lập tức ảnh hưởng đến chủ nhà của nó để làm cho nó bối rối. Sự nhầm lẫn có thể, có thể, dẫn đến một chương trình không rõ ràng, erhm ..

Tên phụ thuộcbất kỳ tên nào trực tiếp hoặc gián tiếp phụ thuộc vào tham số mẫu .

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    
}

Chúng tôi có bốn tên phụ thuộc trong đoạn trích trên:

  • E )
    • "loại" phụ thuộc vào việc khởi tạo SomeTrait<T>, bao gồm T, và;
  • F )
    • "NestedTrait" , là id mẫu , phụ thuộc vào SomeTrait<T>, và;
    • "loại" ở cuối ( F ) phụ thuộc vào NestedTrait , tùy thuộc vào SomeTrait<T>, và;
  • G )
    • "Dữ liệu" , trông giống như một mẫu hàm thành viên , gián tiếp là một tên phụ thuộc vì loại foo phụ thuộc vào việc khởi tạo SomeTrait<T>.

Cả hai câu lệnh ( E ), ( F ) hoặc ( G ) đều không hợp lệ nếu trình biên dịch sẽ hiểu các tên phụ thuộc là các biến / hàm (như đã nêu trước đó là những gì xảy ra nếu chúng ta không nói rõ ràng khác).

GIẢI PHÁP

Để g_tmplcó định nghĩa hợp lệ, chúng ta phải thông báo rõ ràng cho trình biên dịch rằng chúng ta mong đợi một loại trong ( E ), id mẫuloại trong ( F ) và id mẫu trong ( 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
}

Mỗi khi một tên biểu thị một loại, tất cả các tên liên quan phải là tên loại hoặc không gian tên , với ý nghĩ này, thật dễ dàng để thấy rằng chúng tôi áp dụng typenamevào đầu tên đủ điều kiện của chúng tôi .

templatetuy nhiên, là khác nhau về vấn đề này, vì không có cách nào để đi đến kết luận như; "Ồ, đây là một mẫu, vậy thứ khác cũng phải là một mẫu" . Điều này có nghĩa là chúng tôi áp dụng templatetrực tiếp trước bất kỳ tên nào mà chúng tôi muốn xử lý như vậy.



TÔI CÓ THỂ BỎ QUA CÁC TỪ KHÓA TRONG MỌI TÊN KHÔNG?

" Tôi có thể dính typenametemplateđứng trước bất kỳ tên nào không? Tôi không muốn lo lắng về bối cảnh chúng xuất hiện ... " -Some C++ Developer

Các quy tắc trong Tiêu chuẩn quy định rằng bạn có thể áp dụng các từ khóa miễn là bạn đang xử lý một tên đủ điều kiện ( K ), nhưng nếu tên đó không đủ điều kiện , ứng dụng sẽ không được định dạng ( 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

Lưu ý : Áp dụng typenamehoặc templatetrong bối cảnh không bắt buộc không được coi là thực hành tốt; chỉ vì bạn có thể làm một cái gì đó, không có nghĩa là bạn nên làm.


Ngoài ra, có những bối cảnh typenametemplatekhông được phép rõ ràng :

  • Khi chỉ định các cơ sở mà một lớp kế thừa

    Mỗi tên được viết trong danh sách cơ sở-chỉ định cơ sở của lớp dẫn xuất đã được coi là một tên loại , việc chỉ định rõ ràng typenamelà không đúng định dạng và dự phòng.

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


  • Khi id mẫu là cái được nhắc đến trong chỉ thị sử dụng của lớp dẫn xuất

     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;

Tuy nhiên, tôi không chắc việc bạn triển khai inUnion là chính xác. Nếu tôi hiểu chính xác, lớp này không được coi là khởi tạo, do đó tab "fail" sẽ không bao giờ thất bại. Có lẽ sẽ tốt hơn nếu chỉ ra loại đó có trong liên kết hay không với giá trị boolean đơn giản.

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: Hãy xem Boost :: Biến thể

PS2: Có một cái nhìn về những người đánh máy , đáng chú ý trong cuốn sách của Andrei Alexandrescu: Modern C ++ Design


inUnion <U> sẽ được khởi tạo, nếu bạn cố gắng gọi Union <float, bool> :: oper = (U) với U == int. Nó gọi một tập riêng (U, inUnion <U> * = 0).
MSalters

Và công việc với result = true / false là tôi cần boost :: enable_if <>, không tương thích với chuỗi công cụ OSX hiện tại của chúng tôi. Các mẫu riêng biệt vẫn là một ý tưởng tốt, mặc dù.
MSalters

Luc có nghĩa là Typedef Tail :: inUnion <U> dummy; hàng. Điều đó sẽ ngay lập tức Tail. nhưng không inUnion <U>. nó được khởi tạo khi nó cần định nghĩa đầy đủ về nó. điều đó xảy ra ví dụ nếu bạn lấy sizeof hoặc truy cập một thành viên (sử dụng :: foo). @MSalters, dù sao đi nữa, bạn đã gặp phải một vấn đề khác:
Johannes Schaub - litb

-sizeof (U) không bao giờ âm :) vì size_t là kiểu số nguyên không dấu. bạn sẽ nhận được một số rất cao. bạn có thể muốn làm sizeof (U)> = 1? -1: 1 hoặc tương tự :)
Johannes Schaub - litb

tôi sẽ chỉ để nó không xác định và chỉ khai báo nó: template <typename U> struct inUnion; vì vậy nó chắc chắn không thể được khởi tạo. Tôi nghĩ rằng có nó với sizeof, trình biên dịch cũng được phép cung cấp cho bạn một lỗi ngay cả khi bạn không khởi tạo nó, bởi vì nếu biết sizeof (U) luôn luôn> = 1 và ...
Johannes Schaub - litb

20

Câu trả lời này có nghĩa là một câu hỏi khá ngắn gọn và ngọt ngào để trả lời (một phần) câu hỏi có tiêu đề. Nếu bạn muốn có một câu trả lời với nhiều chi tiết giải thích lý do tại sao bạn phải đặt chúng ở đó, vui lòng vào đây .


Nguyên tắc chung để đặt typenametừ khóa chủ yếu là khi bạn đang sử dụng tham số mẫu và bạn muốn truy cập một typedefbí danh lồng nhau hoặc sử dụng, ví dụ:

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

Lưu ý rằng điều này cũng áp dụng cho các hàm meta hoặc những thứ có tham số mẫu chung. Tuy nhiên, nếu tham số mẫu được cung cấp là loại rõ ràng thì bạn không phải chỉ định typename, ví dụ:

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;
};

Các quy tắc chung để thêm templatevòng loại hầu hết tương tự nhau, ngoại trừ chúng thường liên quan đến các hàm thành viên templated (tĩnh hoặc khác) của một cấu trúc / lớp mà chính nó được tạo khuôn mẫu, ví dụ:

Cho cấu trúc và chức năng này:

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
}

Cố gắng truy cập t.get<int>()từ bên trong chức năng sẽ dẫn đến lỗi:

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

Do đó, trong bối cảnh này, bạn sẽ cần templatetừ khóa trước và gọi nó như vậy:

t.template get<int>()

Bằng cách đó, trình biên dịch sẽ phân tích cú pháp này đúng hơn là t.get < int.


2

Tôi đang đặt câu trả lời tuyệt vời của JLBorges cho một câu hỏi tương tự nguyên văn từ cplusplus.com, vì đó là lời giải thích ngắn gọn nhất mà tôi đã đọc về chủ đề này.

Trong một mẫu mà chúng tôi viết, có hai loại tên có thể được sử dụng - tên phụ thuộc và tên không phụ thuộc. Tên phụ thuộc là tên phụ thuộc vào tham số mẫu; một tên không phụ thuộc có cùng ý nghĩa bất kể các tham số mẫu là gì.

Ví dụ:

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)
}

Những gì một tên phụ thuộc đề cập đến có thể là một cái gì đó khác nhau cho mỗi lần khởi tạo khác nhau của mẫu. Kết quả là, các mẫu C ++ phải chịu "tra cứu tên hai pha". Khi một mẫu ban đầu được phân tích cú pháp (trước khi bất kỳ sự khởi tạo nào diễn ra), trình biên dịch sẽ tìm kiếm các tên không phụ thuộc. Khi một sự khởi tạo cụ thể của mẫu diễn ra, các tham số mẫu được biết đến sau đó và trình biên dịch sẽ tìm kiếm các tên phụ thuộc.

Trong giai đoạn đầu tiên, trình phân tích cú pháp cần biết nếu một tên phụ thuộc là tên của một loại hoặc tên của một loại không. Theo mặc định, tên phụ thuộc được coi là tên của loại không. Từ khóa typename trước một tên phụ thuộc xác định rằng đó là tên của một loại.


Tóm lược

Chỉ sử dụng tên kiểu từ khóa trong khai báo và định nghĩa mẫu với điều kiện bạn có tên đủ điều kiện tham chiếu đến một loại và phụ thuộc vào tham số mẫu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.