Khi nào tôi có thể sử dụng khai báo chuyển tiếp?


602

Tôi đang tìm định nghĩa khi nào tôi được phép khai báo trước một lớp trong tệp tiêu đề của lớp khác:

Tôi có được phép làm điều đó cho một lớp cơ sở, cho một lớp được tổ chức như một thành viên, cho một lớp được truyền cho chức năng thành viên bằng cách tham chiếu, v.v.?


14
Tôi rất muốn điều này được đổi tên thành "khi nào tôi nên " và các câu trả lời được cập nhật một cách thích hợp ...
deworde

12
@deworde Khi bạn nói khi nào "nên" bạn đang hỏi ý kiến.
AturSams

@deworde, tôi hiểu rằng bạn muốn sử dụng khai báo chuyển tiếp bất cứ khi nào bạn có thể, để cải thiện thời gian xây dựng và tránh các tham chiếu vòng tròn. Ngoại lệ duy nhất tôi có thể nghĩ đến là khi một tệp bao gồm chứa typedefs, trong trường hợp đó có sự đánh đổi giữa việc xác định lại typedef (và có nguy cơ thay đổi) và bao gồm toàn bộ tệp (cùng với đệ quy bao gồm).
Ohad Schneider

@OhadSchneider Từ góc độ thực tế, tôi không phải là một fan hâm mộ lớn của các tiêu đề mà tôi. ÷
deworde

về cơ bản luôn yêu cầu bạn bao gồm một tiêu đề khác để sử dụng chúng (chuyển tiếp từ chối của tham số hàm tạo là thủ phạm lớn ở đây)
deworde 7/12/2016

Câu trả lời:


962

Đặt mình vào vị trí của trình biên dịch: khi bạn chuyển tiếp khai báo một loại, tất cả trình biên dịch đều biết rằng loại này tồn tại; nó không biết gì về kích thước, thành viên hoặc phương thức của nó. Đây là lý do tại sao nó được gọi là một loại không đầy đủ . Do đó, bạn không thể sử dụng kiểu để khai báo một thành viên hoặc lớp cơ sở, vì trình biên dịch sẽ cần phải biết cách bố trí của kiểu.

Giả sử khai báo sau.

class X;

Đây là những gì bạn có thể và không thể làm.

Những gì bạn có thể làm với một loại không đầy đủ:

  • Khai báo một thành viên là một con trỏ hoặc một tham chiếu đến loại không đầy đủ:

    class Foo {
        X *p;
        X &r;
    };
    
  • Khai báo các hàm hoặc phương thức chấp nhận / trả về các kiểu không hoàn chỉnh:

    void f1(X);
    X    f2();
    
  • Xác định các hàm hoặc phương thức chấp nhận / trả về con trỏ / tham chiếu đến kiểu không hoàn chỉnh (nhưng không sử dụng các thành viên của nó):

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}
    

Những gì bạn không thể làm với một loại không đầy đủ:

  • Sử dụng nó như là một lớp cơ sở

    class Foo : X {} // compiler error!
  • Sử dụng nó để tuyên bố một thành viên:

    class Foo {
        X m; // compiler error!
    };
    
  • Xác định hàm hoặc phương thức sử dụng loại này

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Sử dụng các phương thức hoặc trường của nó, trong thực tế, cố gắng hủy bỏ một biến với kiểu không hoàn chỉnh

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    

Khi nói đến các mẫu, không có quy tắc tuyệt đối: bạn có thể sử dụng loại không hoàn chỉnh làm tham số mẫu hay không phụ thuộc vào cách sử dụng loại trong mẫu.

Ví dụ, std::vector<T>yêu cầu tham số của nó là một kiểu hoàn chỉnh, trong khi boost::container::vector<T>không. Đôi khi, một loại hoàn chỉnh chỉ được yêu cầu nếu bạn sử dụng các chức năng thành viên nhất định; đây là trường hợpstd::unique_ptr<T> ví dụ

Một mẫu tài liệu tốt sẽ chỉ ra trong tài liệu của nó tất cả các yêu cầu của các tham số của nó, bao gồm cả liệu chúng có cần phải là loại hoàn chỉnh hay không.


4
Câu trả lời tuyệt vời nhưng xin vui lòng xem của tôi dưới đây cho các điểm kỹ thuật mà tôi không đồng ý. Nói tóm lại, nếu bạn không bao gồm các tiêu đề cho các loại không hoàn chỉnh mà bạn chấp nhận hoặc trả lại, bạn buộc một sự phụ thuộc vô hình vào người tiêu dùng của tiêu đề của bạn phải biết họ cần loại nào khác.
Andy Dent

2
@AndyDent: Đúng, nhưng người tiêu dùng của tiêu đề chỉ cần bao gồm (các) phụ thuộc mà anh ta thực sự sử dụng, vì vậy điều này tuân theo nguyên tắc C ++ là "bạn chỉ trả tiền cho những gì bạn sử dụng". Nhưng thực sự, nó có thể gây bất tiện cho người dùng, những người mong đợi tiêu đề là độc lập.
Luc Touraille

8
Bộ quy tắc này bỏ qua một trường hợp rất quan trọng: bạn cần một loại hoàn chỉnh để khởi tạo hầu hết các mẫu trong thư viện chuẩn. Cần phải đặc biệt chú ý đến điều này, vì vi phạm quy tắc dẫn đến hành vi không xác định và có thể không gây ra lỗi trình biên dịch.
James Kanze

12
+1 cho "đặt mình vào vị trí của nhà soạn nhạc". Tôi tưởng tượng "trình biên dịch" có ria mép.
PascalVKooten

3
@JesusChrist: Chính xác: khi bạn truyền một đối tượng theo giá trị, trình biên dịch cần biết kích thước của nó để thực hiện thao tác ngăn xếp thích hợp; khi truyền con trỏ hoặc tham chiếu, trình biên dịch không cần kích thước hoặc bố cục của đối tượng, chỉ có kích thước của một địa chỉ (tức là kích thước của một con trỏ), không phụ thuộc vào kiểu được trỏ.
Luc Touraille

45

Nguyên tắc chính là bạn chỉ có thể khai báo chuyển tiếp các lớp có bố cục bộ nhớ (và do đó các hàm thành viên và thành viên dữ liệu) không cần phải biết trong tệp bạn chuyển tiếp khai báo nó.

Điều này sẽ loại trừ các lớp cơ sở và bất cứ thứ gì trừ các lớp được sử dụng thông qua các tham chiếu và con trỏ.


6
Hầu hết. Bạn cũng có thể tham khảo các kiểu không hoàn chỉnh "đơn giản" (nghĩa là không phải con trỏ / tham chiếu) dưới dạng tham số hoặc kiểu trả về trong các nguyên mẫu hàm.
j_random_hacker

Còn các lớp mà tôi muốn sử dụng làm thành viên của một lớp mà tôi định nghĩa trong tệp tiêu đề thì sao? Tôi có thể chuyển tiếp tuyên bố chúng?
Igor Oks

1
Có, nhưng trong trường hợp đó, bạn chỉ có thể sử dụng một tham chiếu hoặc một con trỏ tới lớp khai báo chuyển tiếp. Nhưng nó vẫn cho phép bạn có thành viên.
Reunanen

32

Lakos phân biệt giữa sử dụng lớp

  1. chỉ trong tên (mà khai báo chuyển tiếp là đủ) và
  2. trong kích thước (mà định nghĩa lớp là cần thiết).

Tôi chưa bao giờ thấy nó phát âm ngắn gọn hơn :)


2
Trong tên chỉ có nghĩa là gì?
Boon

4
@Boon: tôi có dám nói không ...? Nếu bạn chỉ sử dụng tên của lớp ?
Marc Mutz - mmutz

1
Thêm một cho Lakos, Marc
mlvljr

28

Cũng như các con trỏ và tham chiếu đến các kiểu không hoàn chỉnh, bạn cũng có thể khai báo các nguyên mẫu hàm xác định các tham số và / hoặc trả về các giá trị là các kiểu không hoàn chỉnh. Tuy nhiên, bạn không thể xác định hàm có tham số hoặc kiểu trả về không đầy đủ, trừ khi đó là con trỏ hoặc tham chiếu.

Ví dụ:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

19

Không có câu trả lời nào cho đến nay mô tả khi nào người ta có thể sử dụng khai báo chuyển tiếp của mẫu lớp. Vì vậy, ở đây nó đi.

Một mẫu lớp có thể được chuyển tiếp khai báo là:

template <typename> struct X;

Theo cấu trúc của câu trả lời được chấp nhận ,

Đây là những gì bạn có thể và không thể làm.

Những gì bạn có thể làm với một loại không đầy đủ:

  • Khai báo một thành viên là một con trỏ hoặc một tham chiếu đến kiểu không hoàn chỉnh trong một mẫu lớp khác:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
    
  • Khai báo một thành viên là một con trỏ hoặc một tham chiếu đến một trong những phần khởi tạo chưa hoàn chỉnh của nó:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Khai báo các mẫu hàm hoặc các mẫu hàm thành viên chấp nhận / trả về các kiểu không hoàn chỉnh:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Khai báo các hàm hoặc các hàm thành viên chấp nhận / trả về một trong các phần khởi tạo không hoàn chỉnh của nó:

    void      f1(X<int>);
    X<int>    f2();
  • Xác định các mẫu hàm hoặc các mẫu hàm thành viên chấp nhận / trả về các con trỏ / tham chiếu đến kiểu không hoàn chỉnh (nhưng không sử dụng các thành viên của nó):

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
  • Xác định các hàm hoặc phương thức chấp nhận / trả về con trỏ / tham chiếu đến một trong các phần khởi tạo không hoàn chỉnh của nó (nhưng không sử dụng các thành viên của nó):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • Sử dụng nó như một lớp cơ sở của một lớp mẫu khác

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Sử dụng nó để khai báo một thành viên của mẫu lớp khác:

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Xác định các mẫu hàm hoặc phương thức sử dụng loại này

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }

Những gì bạn không thể làm với một loại không đầy đủ:

  • Sử dụng một trong các tức thời của nó như là một lớp cơ sở

    class Foo : X<int> {} // compiler error!
  • Sử dụng một trong những lời nhắc của nó để tuyên bố một thành viên:

    class Foo {
        X<int> m; // compiler error!
    };
  • Xác định các hàm hoặc phương thức bằng cách sử dụng một trong các phần khởi tạo của nó

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Sử dụng các phương thức hoặc các trường của một trong các khởi tạo của nó, trong thực tế, cố gắng hủy bỏ một biến với loại không đầy đủ

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Tạo tức thời rõ ràng của mẫu lớp

    template struct X<int>;

2
"Không có câu trả lời nào cho đến nay mô tả khi nào người ta có thể khai báo về phía trước của một mẫu lớp." Không phải điều đó đơn giản chỉ vì ngữ nghĩa của XX<int>hoàn toàn giống nhau, và chỉ có cú pháp khai báo chuyển tiếp khác nhau theo bất kỳ cách thực chất nào, với tất cả ngoại trừ 1 dòng câu trả lời của bạn chỉ bằng cách lấy Luc và s/X/X<int>/g? Điều đó có thực sự cần thiết? Hay tôi đã bỏ lỡ một chi tiết nhỏ khác nhau? Điều đó là có thể, nhưng tôi đã so sánh trực quan một vài lần và không thể thấy bất kỳ ...
underscore_d

Cảm ơn bạn! Chỉnh sửa đó thêm một tấn thông tin có giá trị. Tôi sẽ phải đọc nó nhiều lần để hiểu đầy đủ về nó ... hoặc có thể sử dụng chiến thuật chờ đợi thường xuyên hơn cho đến khi tôi bị nhầm lẫn khủng khiếp trong mã thực và quay lại đây! Tôi nghi ngờ tôi sẽ có thể sử dụng điều này để giảm sự phụ thuộc ở nhiều nơi.
gạch dưới

4

Trong tệp mà bạn chỉ sử dụng Con trỏ hoặc Tham chiếu đến một lớp. Và không có hàm thành viên / thành viên nào được gọi là các con trỏ / tham chiếu đó.

với class Foo;// khai báo chuyển tiếp

Chúng tôi có thể khai báo thành viên dữ liệu loại Foo * hoặc Foo &.

Chúng ta có thể khai báo (nhưng không xác định) các hàm với các đối số và / hoặc trả về các giá trị, loại Foo.

Chúng ta có thể khai báo các thành viên dữ liệu tĩnh của loại Foo. Điều này là do các thành viên dữ liệu tĩnh được định nghĩa bên ngoài định nghĩa lớp.


4

Tôi đang viết đây là một câu trả lời riêng biệt thay vì chỉ là một nhận xét vì tôi không đồng ý với câu trả lời của Luc Touraille, không phải vì lý do hợp pháp mà là phần mềm mạnh mẽ và nguy cơ giải thích sai.

Cụ thể, tôi có một vấn đề với hợp đồng ngụ ý về những gì bạn mong muốn người dùng giao diện của bạn phải biết.

Nếu bạn đang quay lại hoặc chấp nhận các loại tham chiếu, thì bạn chỉ nói rằng họ có thể chuyển qua một con trỏ hoặc tham chiếu mà họ có thể chỉ biết đến thông qua một tuyên bố chuyển tiếp.

Khi bạn trả về một loại không đầy đủ X f2();thì bạn đang nói rằng người gọi của bạn phải có đặc điểm kỹ thuật loại đầy đủ của X. Họ cần nó để tạo LHS hoặc đối tượng tạm thời tại trang web cuộc gọi.

Tương tự, nếu bạn chấp nhận một loại không đầy đủ, người gọi phải xây dựng đối tượng là tham số. Ngay cả khi đối tượng đó được trả về dưới dạng một kiểu không hoàn chỉnh khác từ một hàm, trang web cuộc gọi cần khai báo đầy đủ. I E:

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

Tôi nghĩ có một nguyên tắc quan trọng là một tiêu đề sẽ cung cấp đủ thông tin để sử dụng nó mà không cần phụ thuộc vào các tiêu đề khác. Điều đó có nghĩa là tiêu đề sẽ có thể được bao gồm trong một đơn vị biên dịch mà không gây ra lỗi trình biên dịch khi bạn sử dụng bất kỳ chức năng nào mà nó khai báo.

Ngoại trừ

  1. Nếu phụ thuộc bên ngoài này là hành vi mong muốn . Thay vì sử dụng trình biên dịch có điều kiện, bạn có thể có một yêu cầu được ghi chép rõ ràng để họ cung cấp tiêu đề khai báo X. Đây là cách thay thế cho việc sử dụng #ifdefs và có thể là một cách hữu ích để giới thiệu giả hoặc các biến thể khác.

  2. Sự khác biệt quan trọng là một số kỹ thuật mẫu mà bạn rõ ràng KHÔNG mong muốn khởi tạo chúng, được đề cập chỉ để ai đó không bị ngớ ngẩn với tôi.


"Tôi nghĩ rằng có một nguyên tắc quan trọng là một tiêu đề nên cung cấp đủ thông tin để sử dụng nó mà không cần phụ thuộc vào các tiêu đề khác." - một vấn đề khác được đề cập trong một bình luận của Adrian McCarthy về câu trả lời của Naveen. Điều đó cung cấp một lý do hợp lý để không tuân theo nguyên tắc "nên cung cấp đủ thông tin để sử dụng" ngay cả đối với các loại hiện không được tạo mẫu.
Tony Delroy

3
Bạn đang nói về việc khi nào bạn nên (hoặc không nên) sử dụng khai báo chuyển tiếp. Đó hoàn toàn không phải là điểm của câu hỏi này. Đây là về việc biết các khả năng kỹ thuật khi (ví dụ) muốn phá vỡ một vấn đề phụ thuộc vòng tròn.
JonnyJD

1
I disagree with Luc Touraille's answerVì vậy, viết cho anh ấy một bình luận, bao gồm một liên kết đến một bài đăng blog nếu bạn cần độ dài. Điều này không trả lời câu hỏi. Nếu mọi người nghĩ các câu hỏi về cách X hoạt động các câu trả lời hợp lý không đồng ý với X thực hiện điều đó hoặc tranh luận về các giới hạn trong đó chúng ta nên hạn chế quyền tự do sử dụng X - chúng ta gần như không có câu trả lời thực sự.
gạch dưới

3

Quy tắc chung tôi tuân theo là không bao gồm bất kỳ tệp tiêu đề nào trừ khi tôi phải. Vì vậy, trừ khi tôi lưu trữ đối tượng của một lớp là biến thành viên của lớp tôi, tôi sẽ không bao gồm nó, tôi sẽ chỉ sử dụng khai báo chuyển tiếp.


2
Điều này phá vỡ đóng gói và làm cho mã giòn. Để làm điều này, bạn cần biết loại đó là kiểu typedef hay lớp cho mẫu lớp với các tham số mẫu mặc định và nếu việc triển khai thay đổi, bạn sẽ cần cập nhật từng nơi bạn đã sử dụng khai báo chuyển tiếp.
Adrian McCarthy

@AdrianMcCarthy là đúng, và một giải pháp hợp lý là có một tiêu đề khai báo chuyển tiếp bao gồm tiêu đề có nội dung mà nó chuyển tiếp tuyên bố, cũng nên được sở hữu / duy trì / vận chuyển bởi bất kỳ ai sở hữu tiêu đề đó. Ví dụ: tiêu đề thư viện iosfwd Standard, chứa các khai báo chuyển tiếp của nội dung iostream.
Tony Delroy

3

Miễn là bạn không cần định nghĩa (nghĩ về con trỏ và tham chiếu), bạn có thể thoát khỏi các khai báo chuyển tiếp. Đây là lý do tại sao hầu hết bạn sẽ nhìn thấy chúng trong các tiêu đề trong khi các tệp triển khai thường sẽ kéo tiêu đề cho (các) định nghĩa phù hợp.


0

Bạn thường sẽ muốn sử dụng khai báo chuyển tiếp trong tệp tiêu đề lớp khi bạn muốn sử dụng loại (lớp) khác làm thành viên của lớp. Bạn không thể sử dụng các phương thức lớp được khai báo chuyển tiếp trong tệp tiêu đề vì C ++ chưa biết định nghĩa của lớp đó tại thời điểm đó. Đó là logic bạn phải di chuyển vào các tệp .cpp, nhưng nếu bạn đang sử dụng các hàm mẫu, bạn nên giảm chúng xuống chỉ còn phần sử dụng mẫu và chuyển chức năng đó vào tiêu đề.


Điều này không có ý nghĩa. Một người không thể có một thành viên của một loại không đầy đủ. Bất kỳ tuyên bố nào của lớp phải cung cấp mọi thứ mà tất cả người dùng cần biết về kích thước và bố cục của nó. Kích thước của nó bao gồm kích thước của tất cả các thành viên không tĩnh. Chuyển tiếp - tuyên bố một thành viên khiến người dùng không biết gì về kích thước của nó.
gạch dưới

0

Làm cho nó khai báo về phía trước sẽ nhận được mã của bạn để biên dịch (obj được tạo). Tuy nhiên, liên kết (tạo exe) sẽ không thành công trừ khi các định nghĩa được tìm thấy.


2
Tại sao bao giờ 2 người upvote này? Bạn không nói về những gì câu hỏi đang nói về. Bạn có nghĩa là bình thường - không chuyển tiếp - khai báo các chức năng . Câu hỏi là về khai báo trước của các lớp . Như bạn đã nói "khai báo chuyển tiếp sẽ lấy mã của bạn để biên dịch", hãy giúp tôi: biên dịch class A; class B { A a; }; int main(){}và cho tôi biết điều đó diễn ra như thế nào. Tất nhiên nó sẽ không được biên dịch. Tất cả các câu trả lời thích hợp ở đây giải thích tại sao và bối cảnh chính xác, giới hạn trong đó khai báo chuyển tiếp hợp lệ. Thay vào đó bạn đã viết điều này về một cái gì đó hoàn toàn khác nhau.
gạch dưới

0

Tôi chỉ muốn thêm một điều quan trọng bạn có thể làm với một lớp chuyển tiếp không được đề cập trong câu trả lời của Luc Touraille.

Những gì bạn có thể làm với một loại không đầy đủ:

Xác định các hàm hoặc phương thức chấp nhận / trả về con trỏ / tham chiếu đến kiểu không hoàn chỉnh và chuyển tiếp con trỏ / tham chiếu đến hàm khác.

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

Một mô-đun có thể đi qua một đối tượng của một lớp khai báo chuyển tiếp đến một mô-đun khác.


"Một lớp chuyển tiếp" và "một lớp khai báo chuyển tiếp" có thể bị nhầm lẫn khi đề cập đến hai điều rất khác nhau. Những gì bạn đã viết tiếp theo trực tiếp từ các khái niệm ẩn trong câu trả lời của Luc, vì vậy trong khi nó đã đưa ra một nhận xét tốt khi thêm vào làm rõ, tôi không chắc nó có thể trả lời được không.
gạch dưới

0

Như, Luc Touraille đã giải thích rất rõ về nơi sử dụng và không sử dụng khai báo chuyển tiếp của lớp.

Tôi sẽ chỉ thêm vào đó tại sao chúng ta cần sử dụng nó.

Chúng ta nên sử dụng khai báo Chuyển tiếp bất cứ nơi nào có thể để tránh tiêm phụ thuộc không mong muốn.

Do #includecác tệp tiêu đề được thêm vào nhiều tệp, do đó, nếu chúng ta thêm một tiêu đề vào một tệp tiêu đề khác, nó sẽ thêm phép tiêm phụ thuộc không mong muốn vào các phần khác nhau của mã nguồn có thể tránh được bằng cách thêm #includetiêu đề vào .cppcác tệp bất cứ khi nào có thể thay vì thêm vào tệp tiêu đề khác và sử dụng khai báo chuyển tiếp lớp bất cứ nơi nào có thể trong .hcác tệp tiê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.