Câu trả lời ngắn: để thực hiện x
một tên phụ thuộc, để việc tra cứu được hoãn lại cho đến khi biết được tham số mẫu.
Câu trả lời dài: khi trình biên dịch nhìn thấy một mẫu, nó được cho là thực hiện một số kiểm tra ngay lập tức mà không cần xem tham số mẫu. Những người khác được hoãn lại cho đến khi tham số được biết đến. Nó được gọi là biên dịch hai pha và MSVC không làm điều đó nhưng nó được yêu cầu bởi tiêu chuẩn và được thực hiện bởi các trình biên dịch chính khác. Nếu bạn thích, trình biên dịch phải biên dịch mẫu ngay khi nhìn thấy nó (với một loại biểu diễn cây phân tích nội bộ nào đó) và trì hoãn biên dịch khởi tạo cho đến sau này.
Các kiểm tra được thực hiện trên chính khuôn mẫu, chứ không phải trên các cảnh báo cụ thể của nó, yêu cầu trình biên dịch có thể giải quyết ngữ pháp của mã trong mẫu.
Trong C ++ (và C), để giải quyết ngữ pháp của mã, đôi khi bạn cần biết liệu có phải là một loại hay không. Ví dụ:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
nếu A là một kiểu, nó khai báo một con trỏ (không có tác dụng nào khác ngoài việc làm bóng toàn cầu x
). Nếu A là một đối tượng, đó là phép nhân (và chặn một số toán tử quá tải nó là bất hợp pháp, gán cho một giá trị). Nếu nó sai, lỗi này phải được chẩn đoán trong giai đoạn 1 , theo tiêu chuẩn được xác định là lỗi trong mẫu , không phải trong một số khởi tạo cụ thể của nó. Ngay cả khi mẫu không bao giờ được khởi tạo, nếu A là một int
mã ở trên thì không được định dạng và phải được chẩn đoán, giống như nếu foo
không phải là một mẫu, nhưng là một hàm đơn giản.
Bây giờ, tiêu chuẩn nói rằng các tên không phụ thuộc vào tham số mẫu phải có thể phân giải được trong giai đoạn 1. A
ở đây không phải là tên phụ thuộc, nó đề cập đến cùng một loại bất kể loại nào T
. Vì vậy, nó cần được xác định trước khi mẫu được xác định để được tìm thấy và kiểm tra trong giai đoạn 1.
T::A
sẽ là một cái tên phụ thuộc vào T. Chúng ta không thể biết trong giai đoạn 1 cho dù đó là một loại hay không. Loại cuối cùng sẽ được sử dụng như T
trong một khởi tạo rất có thể chưa được xác định và thậm chí nếu chúng ta không biết loại nào sẽ được sử dụng làm tham số mẫu của chúng tôi. Nhưng chúng tôi phải giải quyết ngữ pháp để thực hiện kiểm tra giai đoạn 1 quý giá của chúng tôi cho các mẫu không định dạng. Vì vậy, tiêu chuẩn có quy tắc cho các tên phụ thuộc - trình biên dịch phải cho rằng chúng không phải là loại, trừ khi đủ điều kiện typename
để xác định rằng chúng là loại hoặc được sử dụng trong các ngữ cảnh rõ ràng nhất định. Ví dụ template <typename T> struct Foo : T::A {};
, thay vì loại A lồng nhau, đó là lỗi trong mã thực hiện khởi tạo (giai đoạn 2), không phải là lỗi trong mẫu (giai đoạn 1).T::A
được sử dụng như một lớp cơ sở và do đó rõ ràng là một loại. Nếu Foo
được khởi tạo với một số loại có thành viên dữ liệuA
Nhưng những gì về một mẫu lớp với một lớp cơ sở phụ thuộc?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
Là một tên phụ thuộc hay không? Với các lớp cơ sở, bất kỳ tên nào cũng có thể xuất hiện trong lớp cơ sở. Vì vậy, chúng ta có thể nói rằng A là một tên phụ thuộc và coi nó là một loại không. Điều này sẽ có tác dụng không mong muốn là mọi tên trong Foo đều phụ thuộc và do đó mọi loại được sử dụng trong Foo (ngoại trừ các loại tích hợp) phải đủ tiêu chuẩn. Bên trong Foo, bạn phải viết:
typename std::string s = "hello, world";
bởi vì std::string
sẽ là một tên phụ thuộc và do đó được coi là không phải loại trừ khi được quy định khác. Ôi!
Một vấn đề thứ hai với việc cho phép mã ưa thích của bạn ( return x;
) là ngay cả khi Bar
được xác định trước Foo
và x
không phải là thành viên trong định nghĩa đó, sau đó ai đó có thể định nghĩa chuyên môn hóa Bar
cho một số loại Baz
, như vậy Bar<Baz>
có thành viên dữ liệu x
và sau đó khởi tạo Foo<Baz>
. Vì vậy, trong phần khởi tạo đó, mẫu của bạn sẽ trả về thành viên dữ liệu thay vì trả về toàn cục x
. Hoặc ngược lại, nếu định nghĩa mẫu cơ sở là Bar
có x
, họ có thể định nghĩa một chuyên ngành mà không có nó, và mẫu của bạn sẽ tìm kiếm một toàn cầu x
để quay trở lại Foo<Baz>
. Tôi nghĩ rằng điều này đã được đánh giá là đáng ngạc nhiên và đau khổ như vấn đề bạn có, nhưng nó âm thầm đáng ngạc nhiên, trái ngược với việc ném một lỗi đáng ngạc nhiên.
Để tránh những vấn đề này, tiêu chuẩn có hiệu lực nói rằng các lớp cơ sở phụ thuộc của các mẫu lớp chỉ không được xem xét để tìm kiếm trừ khi được yêu cầu rõ ràng. Điều này ngăn mọi thứ khỏi bị phụ thuộc chỉ vì nó có thể được tìm thấy trong một cơ sở phụ thuộc. Nó cũng có tác dụng không mong muốn mà bạn đang thấy - bạn phải đủ điều kiện từ lớp cơ sở hoặc không tìm thấy. Có ba cách phổ biến để làm cho A
phụ thuộc:
using Bar<T>::A;
trong lớp - A
bây giờ đề cập đến một cái gì đó trong Bar<T>
, do đó phụ thuộc.
Bar<T>::A *x = 0;
tại điểm sử dụng - Một lần nữa, A
chắc chắn là trong Bar<T>
. Đây là phép nhântypename
không được sử dụng, vì vậy có thể là một ví dụ tồi, nhưng chúng ta sẽ phải đợi cho đến khi khởi tạo để tìm hiểu xem có operator*(Bar<T>::A, x)
trả về giá trị hay không . Ai biết được, có lẽ nó ...
this->A;
tại điểm sử dụng - A
là một thành viên, vì vậy nếu không tham gia Foo
, nó phải thuộc lớp cơ sở, một lần nữa tiêu chuẩn cho biết điều này làm cho nó phụ thuộc.
Quá trình biên dịch hai pha rất khó khăn và khó khăn, đồng thời đưa ra một số yêu cầu đáng ngạc nhiên để có thêm thông báo trong mã của bạn. Nhưng giống như dân chủ, đó có lẽ là cách làm tồi tệ nhất có thể, ngoài tất cả những thứ khác.
Bạn có thể lập luận một cách hợp lý rằng trong ví dụ của bạn, return x;
sẽ không có ý nghĩa nếu x
là một kiểu lồng nhau trong lớp cơ sở, vì vậy ngôn ngữ nên (a) nói rằng đó là một tên phụ thuộc và (2) coi nó là một loại không, và mã của bạn sẽ làm việc mà không có this->
. Ở một mức độ nào đó, bạn là nạn nhân của thiệt hại tài sản thế chấp từ giải pháp cho một vấn đề không áp dụng trong trường hợp của bạn, nhưng vẫn có vấn đề về lớp cơ sở của bạn có khả năng giới thiệu các tên theo bạn bóng tối đó hoặc không có tên bạn nghĩ họ đã có, và một toàn cầu được tìm thấy thay thế.
Bạn cũng có thể lập luận rằng mặc định phải ngược lại với tên phụ thuộc (loại giả sử trừ khi bằng cách nào đó được chỉ định là một đối tượng) hoặc mặc định đó phải nhạy cảm hơn với ngữ cảnh (trong std::string s = "";
, std::string
có thể được đọc thành một loại vì không có gì khác làm cho ngữ pháp ý nghĩa, mặc dù std::string *s = 0;
là mơ hồ). Một lần nữa, tôi không biết làm thế nào các quy tắc đã được đồng ý. Tôi đoán là số lượng trang văn bản sẽ được yêu cầu, giảm thiểu chống lại việc tạo ra nhiều quy tắc cụ thể trong đó bối cảnh lấy một loại và loại không phải là loại.