Tại sao tôi phải truy cập các thành viên lớp cơ sở mẫu thông qua con trỏ này?


199

Nếu các lớp dưới đây không phải là mẫu tôi có thể chỉ cần có xtrong derivedlớp. Tuy nhiên, với mã dưới đây, tôi phải sử dụng this->x. Tại sao?

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
À. Nó có một cái gì đó để làm với tra cứu tên. Nếu ai đó không trả lời sớm, tôi sẽ tra cứu và đăng nó (hiện đang bận).
templatetypedef

@Ed Swangren: Xin lỗi, tôi đã bỏ lỡ nó trong số các câu trả lời được cung cấp khi đăng câu hỏi này. Tôi đã tìm kiếm câu trả lời trong một thời gian dài trước đó.
Ali

6
Điều này xảy ra do tra cứu tên hai pha (mà không phải tất cả các trình biên dịch sử dụng theo mặc định) và các tên phụ thuộc. Có 3 giải pháp cho vấn đề này, ngoài tiền tố xvới this->, đó là: 1) Sử dụng tiền tố base<T>::x, 2) Thêm một câu lệnh using base<T>::x, 3) Sử dụng công cụ biên dịch toàn cục cho phép chế độ cho phép. Những ưu và nhược điểm của các giải pháp này được mô tả trong stackoverflow.com/questions/50321788/ mẹo
George Robinson

Câu trả lời:


274

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 intmã ở trên thì không được định dạng và phải được chẩn đoán, giống như nếu fookhô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::Asẽ 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ư Ttrong 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 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::stringsẽ 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 Fooxkhô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 Barcho một số loại Baz, như vậy Bar<Baz>có thành viên dữ liệu xvà 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à Barx, 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 Aphụ thuộc:

  • using Bar<T>::A;trong lớp - Abâ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, Achắ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 - Alà 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 xlà 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::stringcó 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.


1
Ooh, câu trả lời chi tiết tốt đẹp. Làm rõ một vài điều mà tôi không bao giờ bận tâm để tìm kiếm. :) 1
jalf

20
@jalf: có điều gì tương tự như C ++
Steve Jessop

4
câu trả lời phi thường, tự hỏi nếu câu hỏi có thể phù hợp với faq.
Matthieu M.

Whoa, chúng ta có thể nói bách khoa toàn thư không? highfive Một điểm tinh tế, mặc dù: "Nếu Foo là instantiated với một số loại mà có một thành viên dữ liệu Một thay vì một loại lồng A, đó là một lỗi trong mã làm instantiation (giai đoạn 2), không phải là một lỗi trong mẫu (giai đoạn 1). " Có thể tốt hơn để nói rằng mẫu không bị sai, nhưng đây vẫn có thể là một trường hợp giả định hoặc lỗi logic không chính xác về phía người viết mẫu. Nếu khởi tạo được gắn cờ thực sự là usecase dự định, thì mẫu sẽ sai.
Ionoclast Brigham

1
@ John. Cho rằng một số trình biên dịch thực hiện -fpermissivehoặc tương tự, có, nó có thể. Tôi không biết chi tiết về cách nó được triển khai, nhưng trình biên dịch phải hoãn lại việc giải quyết xcho đến khi nó biết lớp cơ sở tạm thời thực tế T. Vì vậy, về nguyên tắc ở chế độ không cho phép, nó có thể ghi lại thực tế nó đã hoãn lại, trì hoãn nó, thực hiện tra cứu một khi nó có T, và khi tra cứu thành công, văn bản bạn đề xuất. Sẽ là một gợi ý rất chính xác nếu nó chỉ được thực hiện trong trường hợp nó hoạt động: cơ hội người dùng có nghĩa là một số khác xtừ phạm vi khác là khá nhỏ!
Steve Jessop

13

(Câu trả lời gốc từ ngày 10 tháng 1 năm 2011)

Tôi nghĩ rằng tôi đã tìm thấy câu trả lời: Vấn đề GCC: sử dụng một thành viên của lớp cơ sở phụ thuộc vào đối số mẫu . Câu trả lời không cụ thể cho gcc.


Cập nhật: Đáp lại bình luận của mmichael , từ dự thảo N3337 của Tiêu chuẩn C ++ 11:

14.6.2 Tên phụ thuộc [temp.dep]
[...]
3 Trong định nghĩa của mẫu lớp hoặc mẫu lớp, nếu một lớp cơ sở phụ thuộc vào tham số mẫu, phạm vi lớp cơ sở không được kiểm tra trong quá trình tra cứu tên không đủ tiêu chuẩn tại điểm định nghĩa của mẫu lớp hoặc thành viên hoặc trong quá trình khởi tạo mẫu lớp hoặc thành viên.

Cho dù "bởi vì tiêu chuẩn nói như vậy" được tính là một câu trả lời, tôi không biết. Bây giờ chúng ta có thể hỏi tại sao các tiêu chuẩn bắt buộc điều đó nhưng như câu trả lời xuất sắc của Steve Jessop và những người khác chỉ ra, câu trả lời cho câu hỏi sau này khá dài và có thể tranh cãi. Thật không may, khi nói đến Tiêu chuẩn C ++, thường gần như không thể đưa ra một lời giải thích ngắn gọn và khép kín về lý do tại sao tiêu chuẩn bắt buộc một cái gì đó; điều này cũng đúng với câu hỏi sau


11

Các xẩn trong thừa kế. Bạn có thể bỏ ẩn thông qua:

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

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

25
Câu trả lời này không giải thích tại sao nó bị ẩn.
jamesdlin
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.