Tại sao C ++ không cho phép tình bạn kế thừa?


93

Tại sao tình bạn ít nhất không phải là tùy chọn có thể kế thừa trong C ++? Tôi hiểu tính nhạy cảm và phản xạ bị cấm vì những lý do rõ ràng (tôi nói điều này chỉ để bắt đầu câu trả lời trích dẫn câu hỏi thường gặp đơn giản), nhưng thiếu một cái gì đó dọc theo dòng virtual friend class Foo;câu hỏi của tôi. Có ai biết bối cảnh lịch sử đằng sau quyết định này không? Liệu tình bạn có thực sự chỉ là một sự tấn công có giới hạn mà từ đó nó đã được tìm thấy để trở thành một vài công dụng đáng nể?

Chỉnh sửa để làm rõ hơn: Tôi đang nói về tình huống sau đây, không phải trường hợp con cái của A được tiếp xúc với B hoặc với cả B và con của nó. Tôi cũng có thể tưởng tượng việc cấp quyền truy cập tùy chọn cho các ghi đè của các chức năng bạn bè, v.v.

class A {
  int x;
  friend class B;
};

class B {
  // OK as per friend declaration above.
  void foo(A& a, int n) { a.x = n; }
};

class D : public B { /* can't get in A w/o 'friend class D' declaration. */ };

Câu trả lời được chấp nhận: như Loki nói , hiệu ứng có thể được mô phỏng ít nhiều bằng cách tạo các hàm proxy được bảo vệ trong các lớp cơ sở kết bạn, vì vậy không cần nghiêm ngặt để cấp tình bạn cho một lớp hoặc hệ thống phương thức ảo. Tôi không thích sự cần thiết của các proxy soạn sẵn (mà cơ sở kết bạn sẽ trở thành một cách hiệu quả), nhưng tôi cho rằng điều này được cho là thích hợp hơn một cơ chế ngôn ngữ có nhiều khả năng bị lạm dụng trong hầu hết thời gian. Tôi nghĩ có lẽ đã đến lúc tôi mua và đọc The Design and Evolution of C ++ của Stroupstrup , cuốn sách mà tôi đã thấy đủ người ở đây giới thiệu, để có cái nhìn sâu sắc hơn về những loại câu hỏi này ...

Câu trả lời:


93

Bởi vì tôi có thể viết Foovà bạn của nóBar (do đó có một mối quan hệ tin cậy).

Nhưng tôi có tin tưởng những người viết các lớp bắt nguồn từ đó Barkhông?
Không hẳn. Vì vậy, họ không nên kế thừa tình bạn.

Bất kỳ thay đổi nào trong biểu diễn nội bộ của một lớp sẽ yêu cầu sửa đổi bất kỳ thứ gì phụ thuộc vào biểu diễn đó. Vì vậy, tất cả các thành viên của một lớp và tất cả bạn bè của lớp sẽ yêu cầu sửa đổi.

Do đó, nếu các đại diện nội bộ Foođược sửa đổi sau đó Barcũng phải được sửa đổi (vì tình bạn chặt chẽ liên kết Barđến Foo). Nếu tình bạn được kế thừa thì tất cả các lớp bắt nguồn từ Barđó cũng sẽ bị ràng buộc chặt chẽ Foovà do đó yêu cầu sửa đổi nếu Foođại diện bên trong của bị thay đổi. Nhưng tôi không có kiến ​​thức về các loại dẫn xuất (tôi cũng không nên. Chúng thậm chí có thể được phát triển bởi các công ty khác nhau, v.v.). Vì vậy, tôi sẽ không thể thay đổi Foovì làm như vậy sẽ đưa các thay đổi phá vỡ vào cơ sở mã (vì tôi không thể sửa đổi tất cả các lớp bắt nguồn từBar ).

Vì vậy, nếu tình bạn được kế thừa, bạn đang vô tình giới hạn khả năng sửa đổi một lớp. Điều này là không mong muốn vì bạn về cơ bản làm cho khái niệm API công khai trở nên vô dụng.

Lưu ý: Một phần tử con của Barcó thể truy cập Foobằng cách sử dụng Bar, chỉ cần đặt phương thức trong vùng Barbảo vệ. Sau đó, con của Barcó thể truy cập a Foobằng cách gọi thông qua lớp cha của nó.

Đây có phải là những gì bạn muốn?

class A
{
    int x;
    friend class B;
};

class B
{
    protected:
       // Now children of B can access foo
       void foo(A& a, int n) { a.x = n; }
};

class D : public B
{
    public:
        foo(A& a, int n)
        {
            B::foo(a, n + 5);
        }
};

4
Chơi lô tô. Đó là tất cả về việc hạn chế thiệt hại bạn có thể gây ra bằng cách thay đổi nội bộ của một lớp.
j_random_hacker

Thành thật mà nói, trường hợp tôi thực sự đang nghĩ đến đây là mô hình Luật sư-Khách hàng, nơi một trung gian hoạt động như một giao diện hạn chế đối với các lớp bên ngoài bằng cách trình bày các phương thức trình bao bọc cho lớp truy cập hạn chế bên dưới. Nói rằng một giao diện có sẵn cho tất cả các phần tử con của các lớp khác chứ không phải là một lớp chính xác sẽ hữu ích hơn nhiều so với hệ thống hiện tại.
Jeff

@Jeff: Việc hiển thị đại diện nội bộ cho tất cả các phần tử con của một lớp sẽ khiến mã trở nên không thể thay đổi (nó cũng thực sự phá vỡ tính đóng gói vì bất kỳ ai muốn truy cập các thành viên nội bộ chỉ phải kế thừa từ Bar ngay cả khi họ không thực sự là Bar ).
Martin York

@Martin: Đúng vậy, trong sơ đồ này, cơ sở kết bạn có thể được sử dụng để tham gia lớp kết bạn, điều này có thể là vi phạm đơn giản về tính đóng gói trong nhiều trường hợp (nếu không phải là hầu hết). Tuy nhiên, trong các tình huống mà cơ sở kết bạn là một lớp trừu tượng, bất kỳ lớp dẫn xuất nào cũng sẽ bị ép buộc phải triển khai một giao diện tương hỗ của riêng nó. Tôi không chắc liệu một lớp 'kẻ mạo danh' trong trường hợp này có được coi là đang phá vỡ tính đóng gói hay vi phạm hợp đồng giao diện hay không nếu nó không cố gắng thực hiện đúng vai trò được tuyên bố của mình.
Jeff

@Martin: Đúng vậy, đó là hiệu ứng mà tôi muốn và đôi khi thực sự đã sử dụng rồi, trong đó A thực sự là một giao diện kết bạn có đi có lại đối với một số lớp truy cập hạn chế Z. Khiếu nại phổ biến với Thành ngữ Luật sư-Khách hàng thông thường dường như là lớp giao diện A phải soạn sẵn các trình bao bọc cuộc gọi tới Z và để mở rộng quyền truy cập vào các lớp con, bản soạn thảo của A về cơ bản phải được sao chép trong mọi lớp cơ sở như B. Một giao diện thông thường thể hiện chức năng mà mô-đun muốn cung cấp, chứ không phải chức năng nào trong các lớp khác. muốn sử dụng chính nó.
Jeff

48

Tại sao tình bạn ít nhất không phải là tùy chọn có thể kế thừa trong C ++?

Tôi nghĩ rằng câu trả lời cho câu hỏi đầu tiên của bạn là trong câu hỏi này: "Bạn bè của cha bạn có quyền truy cập vào trang tư nhân của bạn không?"


36
Công bằng mà nói, câu hỏi đó làm dấy lên những lo lắng về cha bạn. . .
iheanyi

3
Mục đích của câu trả lời này là gì? tốt nhất là một nhận xét không rõ ràng mặc dù có thể nhẹ nhàng
Nhà phát

11

Một lớp kết bạn có thể tiết lộ bạn bè của mình thông qua các chức năng của người truy cập, và sau đó cấp quyền truy cập thông qua các chức năng đó.

class stingy {
    int pennies;
    friend class hot_girl;
};

class hot_girl {
public:
    stingy *bf;

    int &get_cash( stingy &x = *bf ) { return x.pennies; }
};

class moocher {
public: // moocher can access stingy's pennies despite not being a friend
    int &get_cash( hot_girl &x ) { return x.get_cash(); }
};

Điều này cho phép kiểm soát tốt hơn độ nhạy tùy chọn. Ví dụ: get_cashcó thể được protectedhoặc có thể thực thi một giao thức truy cập giới hạn thời gian chạy.


@Hector: Bỏ phiếu tái cấu trúc! ;)
Alexander Shukaev

7

Tiêu chuẩn C ++, phần 11.4 / 8

Tình bạn không có tính kế thừa cũng không có tính bắc cầu.

Nếu tình bạn sẽ được kế thừa, thì một lớp không phải là bạn bè sẽ đột nhiên có quyền truy cập vào nội bộ lớp của bạn và điều đó vi phạm tính đóng gói.


2
Giả sử Q là "bạn bè" A, và B có nguồn gốc từ A. Nếu B kế thừa tình bạn từ A, thì vì B là một loại của A, về mặt kỹ thuật, nó LÀ một A có quyền truy cập vào các tài sản riêng của Q. Vì vậy, điều này không trả lời câu hỏi với bất kỳ lý do thực tế nào.
mdenton8

2

Bởi vì nó chỉ là không cần thiết.

Việc sử dụng friendtừ khóa chính nó là đáng ngờ. Về mặt kết hợp, đó là mối quan hệ tồi tệ nhất (đi trước sự kế thừa và thành phần).

Bất kỳ sự thay đổi nào đối với nội bộ của một lớp đều có nguy cơ ảnh hưởng đến những người bạn của lớp này ... bạn có thực sự muốn có một số lượng bạn bè không? Bạn thậm chí sẽ không thể liệt kê chúng nếu những người thừa kế từ chúng cũng có thể là bạn bè, và bạn sẽ có nguy cơ phá mã khách hàng của mình mỗi lần, chắc chắn điều này là không mong muốn.

Tôi tự do thừa nhận rằng đối với bài tập về nhà / dự án vật nuôi, sự phụ thuộc thường là một vấn đề xa vời. Đối với các dự án kích thước nhỏ, điều đó không thành vấn đề. Nhưng ngay khi nhiều người làm việc trong cùng một dự án và điều này phát triển thành hàng chục nghìn dòng, bạn cần hạn chế tác động của các thay đổi.

Điều này mang lại một quy tắc rất đơn giản:

Thay đổi nội bộ của một lớp chỉ ảnh hưởng đến chính lớp đó

Tất nhiên, bạn có thể sẽ ảnh hưởng đến bạn bè của nó, nhưng có hai trường hợp ở đây:

  • Chức năng kết bạn miễn phí: dù sao thì có lẽ nhiều chức năng thành viên hơn (tôi nghĩ std::ostream& operator<<(...)ở đây không phải là thành viên hoàn toàn do ngẫu nhiên của các quy tắc ngôn ngữ
  • bạn cùng lớp? bạn không cần lớp học bạn bè trên lớp học thực.

Tôi khuyên bạn nên sử dụng phương pháp đơn giản:

class Example;

class ExampleKey { friend class Example; ExampleKey(); };

class Restricted
{
public:
  void forExampleOnly(int,int,ExampleKey const&);
};

Mẫu đơn giản này Keycho phép bạn khai báo một người bạn (theo một cách nào đó) mà không thực sự cấp cho nó quyền truy cập vào nội bộ của bạn, do đó cô lập nó khỏi những thay đổi. Hơn nữa, nó cho phép người bạn này cho người được ủy thác mượn chìa khóa (như trẻ em) nếu được yêu cầu.


0

Phỏng đoán: Nếu một lớp tuyên bố một số lớp / chức năng khác là bạn, thì đó là vì thực thể thứ hai đó cần quyền truy cập đặc quyền vào thực thể đầu tiên. Có công dụng gì khi cấp cho thực thể thứ hai quyền truy cập đặc quyền vào một số lớp tùy ý bắt nguồn từ lớp đầu tiên?


2
Nếu một lớp A muốn trao tình bạn cho B và các hậu duệ của nó, nó có thể tránh cập nhật giao diện của mình cho mỗi lớp con được thêm vào hoặc buộc B phải viết bảng mẫu chuyển tiếp, đó là một nửa điểm của tình bạn ngay từ đầu.
Jeff

@Jeff: À, sau đó tôi đã hiểu sai ý của bạn. Tôi giả định bạn có nghĩa là Bsẽ có quyền truy cập vào tất cả các lớp kế thừa từ A...
Oliver Charlesworth

0

Một lớp dẫn xuất chỉ có thể kế thừa một thứ gì đó, là 'thành viên' của cơ sở. Tuyên bố kết bạn không phải là thành viên của lớp học kết bạn.

$ 11,4 / 1- "... Tên của một người bạn không thuộc phạm vi của lớp và người bạn đó không được gọi bằng các toán tử truy cập thành viên (5.2.5) trừ khi đó là thành viên của lớp khác."

$ 11,4 - "Ngoài ra, vì mệnh đề cơ sở của lớp bạn không phải là một phần của các khai báo thành viên của nó, nên mệnh đề cơ sở của lớp bạn không thể truy cập vào tên của các thành viên riêng tư và được bảo vệ khỏi lớp cấp tình bạn."

và xa hơn

$ 10,3 / 7- "[Lưu ý: hàm chỉ định ảo ngụ ý thành viên, do đó, một hàm ảo không thể là một hàm nonmember (7.1.2). Một hàm ảo cũng không thể là một thành viên tĩnh, vì một lệnh gọi hàm ảo dựa vào một đối tượng cụ thể cho xác định hàm nào sẽ gọi. Một hàm ảo được khai báo trong một lớp có thể được khai báo là bạn trong lớp khác.] "

Vì 'friend' không phải là thành viên của lớp cơ sở ngay từ đầu, làm thế nào nó có thể được kế thừa bởi lớp dẫn xuất?


Tình bạn, mặc dù được đưa ra thông qua các tuyên bố như thành viên, không thực sự là thành viên nhiều như các thông báo mà các lớp khác về cơ bản có thể bỏ qua các phân loại khả năng hiển thị trên các thành viên 'thực'. Mặc dù các phần thông số kỹ thuật bạn trích dẫn giải thích cách ngôn ngữ hoạt động liên quan đến các sắc thái và hành vi khung này theo thuật ngữ tự nhất quán, mọi thứ có thể được tạo ra theo cách khác và không có gì ở trên đi vào trung tâm của cơ sở lý luận, thật không may.
Jeff

0

Hàm Friend trong một lớp gán thuộc tính bên ngoài cho hàm. tức là extern có nghĩa là hàm đã được khai báo và định nghĩa ở đâu đó ngoài lớp.

Do đó, nó có nghĩa là hàm bạn bè không phải là thành viên của một lớp. Vì vậy, kế thừa chỉ cho phép bạn kế thừa các thuộc tính của một lớp chứ không phải những thứ bên ngoài. Và cũng nếu kế thừa được cho phép đối với các hàm bạn bè, thì lớp bên thứ ba sẽ kế thừa.


0

Bạn rất giỏi trong việc kế thừa như giao diện kiểu cho vùng chứa Nhưng đối với tôi, như điều đầu tiên đã nói, C ++ thiếu tính kế thừa có thể truyền được

class Thing;

//an interface for Thing container's
struct IThing {
   friend Thing;
   protected:
       int IThing_getData() = 0;
};

//container for thing's
struct MyContainer : public IThing {
    protected: //here is reserved access to Thing
         int IThing_getData() override {...}
};

struct Thing {
    void setYourContainer(IThing* aContainerOfThings) {
        //access to unique function in protected area 
        aContainerOfThings->IThing_getData(); //authorized access
    }
};

struct ChildThing : public Thing {
    void doTest() {
        //here the lack of granularity, you cannot access to the container.
        //to use the container, you must implement all 
        //function in the Thing class
        aContainerOfThings->IThing_getData(); //forbidden access
    }
};

Đối với tôi, vấn đề của C ++ là thiếu tính chi tiết rất tốt để kiểm soát tất cả truy cập từ mọi nơi cho bất kỳ thứ gì:

bạn Thing có thể trở thành bạn của Thing. * để cấp quyền truy cập cho tất cả con của Thing

Và hơn thế nữa, người bạn [khu vực được đặt tên] Thing. * Để cấp quyền truy cập cho chính xác đang ở trong lớp Container thông qua khu vực được đặt tên đặc biệt cho người bạn.

Ok thôi giấc mơ. Nhưng bây giờ, bạn biết một cách sử dụng thú vị của friend.

Theo một thứ tự khác, bạn cũng có thể thấy thú vị khi được biết tất cả các lớp đều thân thiện với bản thân. Nói cách khác, một cá thể lớp có thể gọi tất cả các
thành viên của một cá thể khác có cùng tên mà không bị hạn chế:

class Object {
     private:
         void test() {}
     protected:
         void callAnotherTest(Object* anotherObject) {
             //private, but yes you can call test() from 
             //another object instance
             anotherObject)->test(); 
         }
};

0

Logic đơn giản: 'Tôi có một người bạn Jane. Chỉ vì chúng ta đã trở thành bạn ngày hôm qua không khiến tất cả bạn bè của cô ấy trở thành của tôi. '

Tôi vẫn cần phải chấp thuận những tình bạn cá nhân đó, và mức độ tin cậy sẽ tương ứng.

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.