เหตุใด C ++ จึงไม่อนุญาตให้สืบทอดมิตรภาพ


95

เหตุใดอย่างน้อยมิตรภาพจึงไม่สามารถสืบทอดได้ใน C ++? ฉันเข้าใจว่าการเคลื่อนย้ายและการสะท้อนกลับถูกห้ามด้วยเหตุผลที่ชัดเจน (ฉันพูดสิ่งนี้เพื่อตัดคำตอบของใบเสนอราคาคำถามที่พบบ่อยธรรมดา ๆ เท่านั้น) แต่ฉันขาดบางสิ่งบางอย่างตามแนวของvirtual friend class Foo;ปริศนา มีใครทราบความเป็นมาทางประวัติศาสตร์เบื้องหลังการตัดสินใจครั้งนี้บ้าง? มิตรภาพเป็นเพียงการแฮ็คที่ จำกัด ซึ่งนับตั้งแต่นั้นมาก็กลายเป็นการใช้งานที่น่านับถือที่คลุมเครือไม่กี่อย่าง?

แก้ไขเพื่อความกระจ่าง:ฉันกำลังพูดถึงสถานการณ์ต่อไปนี้ไม่ใช่ที่ที่เด็กของ A สัมผัสกับ B หรือทั้ง B และลูก ๆ ของมัน ฉันยังสามารถจินตนาการได้ว่าการอนุญาตให้เข้าถึงการลบล้างฟังก์ชันของเพื่อนเป็นต้น

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. */ };

คำตอบที่ได้รับการยอมรับ:ในฐานะที่เป็นLokiสามารถจำลองเอฟเฟกต์ได้มากหรือน้อยโดยการสร้างฟังก์ชันพร็อกซีที่มีการป้องกันในคลาสพื้นฐานที่เป็นเพื่อนดังนั้นจึงไม่จำเป็นต้องเข้มงวดในการมอบมิตรภาพให้กับคลาสหรือวิธีการสืบสกุลเสมือน ฉันไม่ชอบความจำเป็นในการใช้พร็อกซีสำเร็จรูป (ซึ่งฐานที่เป็นเพื่อนกลายเป็นได้อย่างมีประสิทธิภาพ) แต่ฉันคิดว่าสิ่งนี้เป็นที่นิยมมากกว่ากลไกภาษาที่มักจะใช้ในทางที่ผิด ฉันคิดว่าคงถึงเวลาที่ฉันซื้อและอ่านThe Design and Evolution of C ++ของ Stroupstrup ซึ่งฉันเคยเห็นคนแนะนำมามากพอที่จะเข้าใจคำถามประเภทนี้ได้ดีขึ้น ...

คำตอบ:


94

เพราะฉันอาจเขียนFooและเป็นเพื่อนของมันBar(ดังนั้นจึงมีความสัมพันธ์ที่ไว้วางใจ)

แต่ฉันจะเชื่อใจคนที่เขียนคลาสที่ได้มาจากBarไหน?
ไม่จริง. ดังนั้นพวกเขาจึงไม่ควรสืบทอดมิตรภาพ

การเปลี่ยนแปลงใด ๆ ในการเป็นตัวแทนภายในของคลาสจะต้องมีการปรับเปลี่ยนอะไรก็ตามที่ขึ้นอยู่กับการเป็นตัวแทนนั้น ดังนั้นสมาชิกทุกคนในชั้นเรียนและเพื่อนทุกคนในชั้นเรียนจะต้องมีการปรับเปลี่ยน

ดังนั้นหากการแสดงภายในของFooการแก้ไขแล้วBarยังต้องได้รับการแก้ไข (เพราะมิตรภาพแน่นผูกBarไปFoo) หากมิตรภาพได้รับการสืบทอดคลาสทั้งหมดที่ได้มาBarก็จะผูกพันกันอย่างแน่นหนาFooดังนั้นจึงต้องมีการปรับเปลี่ยนหากFooการเป็นตัวแทนภายในมีการเปลี่ยนแปลง แต่ฉันไม่มีความรู้เกี่ยวกับประเภทที่ได้รับมา (และฉันควรจะได้รับการพัฒนาโดย บริษัท ที่แตกต่างกัน ฯลฯ ) ดังนั้นฉันจะไม่สามารถเปลี่ยนแปลงได้Fooเนื่องจากการทำเช่นนั้นจะแนะนำการเปลี่ยนแปลงที่ทำลายลงในฐานรหัส (เนื่องจากฉันไม่สามารถแก้ไขคลาสทั้งหมดที่ได้มาจากBar )

ดังนั้นหากมีการสืบทอดมิตรภาพคุณจะแนะนำข้อ จำกัด เกี่ยวกับความสามารถในการปรับเปลี่ยนคลาสโดยไม่ได้ตั้งใจ สิ่งนี้ไม่เป็นที่พึงปรารถนาเนื่องจากคุณทำให้แนวคิดของ API สาธารณะที่ไร้ประโยชน์

หมายเหตุ: ลูกของBarสามารถเข้าถึงได้Fooโดยใช้Barเพียงแค่สร้างวิธีการBarป้องกัน จากนั้นลูกของBarสามารถเข้าถึงได้Fooโดยการเรียกผ่านคลาสแม่

นี่คือสิ่งที่คุณต้องการหรือไม่?

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
บิงโก ทั้งหมดนี้เกี่ยวกับการจำกัดความเสียหายที่คุณสามารถเกิดได้โดยการเปลี่ยนภายในของชั้นเรียน
j_random_hacker

ตรงไปตรงมากรณีที่ฉันคิดถึงเรื่องนี้เป็นรูปแบบ Attorney-Client โดยตัวกลางทำหน้าที่เป็นอินเทอร์เฟซที่ จำกัด สำหรับคลาสภายนอกโดยนำเสนอวิธีการห่อหุ้มให้กับคลาสการเข้าถึงที่ จำกัด การบอกว่าอินเทอร์เฟซพร้อมใช้งานสำหรับเด็กทุกคนในชั้นเรียนอื่น ๆ แทนที่จะเป็นคลาสที่แน่นอนจะมีประโยชน์มากกว่าระบบในปัจจุบัน
Jeff

@ เจฟฟ์: การเปิดเผยการเป็นตัวแทนภายในให้กับเด็ก ๆ ทุกคนในคลาสจะทำให้โค้ดไม่สามารถเปลี่ยนแปลงได้ (มันยังทำลายการห่อหุ้มเนื่องจากใครก็ตามที่ต้องการเข้าถึงสมาชิกภายในจะต้องสืบทอดจากบาร์แม้ว่าพวกเขาจะไม่ใช่บาร์จริงๆก็ตาม ).
Martin York

@ มาร์ติน: ใช่แล้วในรูปแบบนี้ฐานเพื่อนสามารถใช้เพื่อรับในชั้นเรียนเพื่อนซึ่งอาจเป็นการละเมิดการห่อหุ้มอย่างง่ายในหลาย ๆ กรณี (ถ้าไม่ใช่ส่วนใหญ่) อย่างไรก็ตามในสถานการณ์ที่ฐานเพื่อนเป็นคลาสนามธรรมคลาสที่ได้รับจะถูกบีบบังคับให้ใช้อินเทอร์เฟซซึ่งกันและกันของตัวเอง ฉันไม่แน่ใจว่าคลาส 'นักต้มตุ๋น' ในสถานการณ์นี้จะถูกพิจารณาว่าทำลายการห่อหุ้มหรือละเมิดสัญญาอินเทอร์เฟซหากไม่พยายามปฏิบัติตามบทบาทที่อ้างสิทธิ์อย่างถูกต้องอย่างซื่อสัตย์
Jeff

@Martin: ใช่นั่นคือเอฟเฟกต์ที่ฉันต้องการและบางครั้งก็ใช้งานจริงอยู่แล้วโดยที่ A เป็นอินเทอร์เฟซที่เป็นมิตรซึ่งกันและกันกับคลาสการเข้าถึงที่ จำกัด บางอย่าง Z การร้องเรียนทั่วไปเกี่ยวกับ Attorney-Client Idiom ดูเหมือนว่าคลาสอินเทอร์เฟซ A ต้องใช้ตัวห่อการเรียกแบบสำเร็จรูปเป็น Z และเพื่อที่จะขยายการเข้าถึงคลาสย่อยได้ต้องมีการทำสำเนาต้นแบบของ A ในทุกคลาสพื้นฐานเช่น B โดยปกติแล้วอินเทอร์เฟซจะแสดงฟังก์ชันการทำงานที่โมดูลต้องการนำเสนอไม่ใช่ฟังก์ชันใดในโมดูลอื่น ๆ ต้องการใช้เอง
Jeff

48

เหตุใดอย่างน้อยมิตรภาพจึงไม่สามารถสืบทอดได้ใน C ++?

ฉันคิดว่าคำตอบสำหรับคำถามแรกของคุณอยู่ที่คำถามนี้: "เพื่อนของพ่อของคุณสามารถเข้าถึงห้องส่วนตัวของคุณได้หรือไม่"


36
เพื่อความเป็นธรรมคำถามนั้นก่อให้เกิดความไม่พอใจเกี่ยวกับพ่อของคุณ . .
iheanyi

3
ประเด็นของคำตอบนี้คืออะไร? ที่ดีที่สุดเป็นที่น่าสงสัยแม้ว่าอาจจะเป็นความคิดเห็นที่ไม่เบาก็ตาม
DeveloperChris

11

ชั้นเรียนที่เป็นเพื่อนอาจเปิดเผยเพื่อนของตนผ่านทางฟังก์ชั่นการเข้าถึงจากนั้นให้สิทธิ์การเข้าถึงผ่านทางเหล่านั้น

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

ซึ่งช่วยให้สามารถควบคุมได้ดีกว่าการขนส่งทางเลือก ตัวอย่างเช่นget_cashอาจเป็นprotectedหรือบังคับใช้โปรโตคอลของการเข้าถึงแบบ จำกัด รันไทม์


@ เฮคเตอร์: โหวตให้ refactoring! ;)
Alexander Shukaev

8

มาตรฐาน C ++ มาตรา 11.4 / 8

มิตรภาพไม่ได้รับการถ่ายทอดหรือสกรรมกริยา

ถ้าจะสืบทอดมิตรภาพชั้นเรียนที่ไม่ได้ตั้งใจจะเป็นเพื่อนก็จะสามารถเข้าถึงภายในชั้นเรียนของคุณได้ในทันใดและนั่นเป็นการละเมิดการห่อหุ้ม


2
พูดว่า Q "เพื่อน" A และ B มาจาก A ถ้า B สืบทอดมิตรภาพจาก A ดังนั้นเนื่องจาก B เป็นประเภท A ในทางเทคนิคจึงเป็น A ที่สามารถเข้าถึงส่วนตัวของ Q ได้ ดังนั้นนี่จึงไม่ตอบคำถามด้วยเหตุผลที่เป็นประโยชน์
mdenton8

2

เพราะมันไม่จำเป็น

การใช้งาน friendคำหลักนั้นน่าสงสัย ในแง่ของการมีเพศสัมพันธ์เป็นความสัมพันธ์ที่แย่ที่สุด (ก่อนการถ่ายทอดทางพันธุกรรมและองค์ประกอบ)

การเปลี่ยนแปลงภายในของชั้นเรียนใด ๆ มีความเสี่ยงที่จะส่งผลกระทบต่อเพื่อนในชั้นเรียนนี้ ... คุณต้องการเพื่อนที่ไม่รู้จักจำนวนมากหรือไม่? คุณจะไม่สามารถระบุรายชื่อพวกเขาได้หากผู้ที่ได้รับมรดกจากพวกเขาสามารถเป็นเพื่อนได้เช่นกันและคุณจะเสี่ยงต่อการทำลายรหัสลูกค้าของคุณทุกครั้งซึ่งไม่เป็นที่ต้องการอย่างแน่นอน

ฉันยอมรับอย่างอิสระว่าสำหรับการบ้าน / โครงการสัตว์เลี้ยงมักเป็นเรื่องที่ต้องพิจารณา ในโครงการขนาดเล็กไม่สำคัญ แต่ทันทีที่หลาย ๆ คนทำงานในโครงการเดียวกันและสิ่งนี้เพิ่มขึ้นเป็นหลายหมื่นบรรทัดคุณต้อง จำกัด ผลกระทบของการเปลี่ยนแปลง

สิ่งนี้นำมาซึ่งกฎง่ายๆ:

การเปลี่ยนภายในของคลาสควรมีผลกับคลาสเท่านั้น

แน่นอนคุณอาจส่งผลกระทบต่อเพื่อน แต่มีสองกรณีที่นี่:

  • ฟังก์ชั่นเพื่อนฟรี: น่าจะเป็นฟังก์ชันสมาชิกมากกว่า (ฉันคิดว่า std::ostream& operator<<(...)ที่นี่ไม่ใช่สมาชิกโดยบังเอิญจากกฎภาษา
  • เพื่อนชั้น? คุณไม่ต้องการชั้นเรียนเพื่อนในชั้นเรียนจริง

ฉันอยากจะแนะนำให้ใช้วิธีง่ายๆ:

class Example;

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

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

Keyรูปแบบง่ายๆนี้ช่วยให้คุณสามารถประกาศเพื่อน (ในทางหนึ่ง) โดยไม่ต้องให้สิทธิ์เข้าถึงภายในของคุณจริงๆดังนั้นจึงแยกมันออกจากการเปลี่ยนแปลง นอกจากนี้ยังอนุญาตให้เพื่อนคนนี้ยืมกุญแจผู้ดูแล (เช่นลูก ๆ ) ได้หากจำเป็น


0

คำเดา: ถ้าชั้นเรียนประกาศคลาส / ฟังก์ชันอื่น ๆ เป็นเพื่อนนั่นเป็นเพราะเอนทิตีที่สองนั้นต้องการการเข้าถึงที่มีสิทธิพิเศษสำหรับคลาสแรก มีประโยชน์อะไรในการให้สิทธิ์การเข้าถึงของเอนทิตีที่สองไปยังคลาสที่ได้รับจากคลาสแรกโดยพลการ


2
หากคลาส A ต้องการมอบมิตรภาพให้กับ B และลูกหลานก็สามารถหลีกเลี่ยงการอัปเดตอินเทอร์เฟซสำหรับคลาสย่อยแต่ละคลาสที่เพิ่มเข้ามาหรือบังคับให้ B เขียนหนังสือต้นแบบแบบพาส - ทรูซึ่งถือเป็นครึ่งหนึ่งของมิตรภาพในตอนแรก
Jeff

@ เจฟฟ์: อ่าแล้วฉันเข้าใจความหมายที่ตั้งใจของคุณผิด ฉันคิดว่าคุณหมายความว่าBจะสามารถเข้าถึงคลาสทั้งหมดที่สืบทอดมาจากA...
Oliver Charlesworth

0

คลาสที่ได้รับสามารถสืบทอดได้เฉพาะบางสิ่งซึ่งก็คือ 'สมาชิก' ของฐาน ประกาศว่าเป็นเพื่อนไม่ได้เป็นสมาชิกของชั้นเรียนตีสนิท

$ 11.4 / 1- "... ชื่อของเพื่อนไม่อยู่ในขอบเขตของชั้นเรียนและเพื่อนจะไม่ถูกเรียกด้วยตัวดำเนินการเข้าถึงสมาชิก (5.2.5) เว้นแต่จะเป็นสมาชิกของคลาสอื่น"

$ 11.4 - "นอกจากนี้เนื่องจากประโยคพื้นฐานของคลาสเพื่อนไม่ได้เป็นส่วนหนึ่งของการประกาศเกี่ยวกับสมาชิกประโยคฐานของคลาสเพื่อนจึงไม่สามารถเข้าถึงชื่อของสมาชิกส่วนตัวและได้รับการปกป้องจากคลาสที่มอบมิตรภาพให้"

และต่อไป

$ 10.3 / 7- "[หมายเหตุ: ตัวระบุเสมือนแสดงถึงการเป็นสมาชิกดังนั้นฟังก์ชันเสมือนจึงไม่สามารถเป็นฟังก์ชันที่ไม่ใช่สมาชิก (7.1.2) และฟังก์ชันเสมือนไม่สามารถเป็นสมาชิกแบบคงที่ได้เนื่องจากการเรียกฟังก์ชันเสมือนจะอาศัยออบเจ็กต์เฉพาะสำหรับ การกำหนดฟังก์ชันที่จะเรียกใช้ฟังก์ชันเสมือนที่ประกาศในคลาสหนึ่งสามารถประกาศว่าเป็นเพื่อนในคลาสอื่น] "

เนื่องจาก 'เพื่อน' ไม่ได้เป็นสมาชิกของคลาสพื้นฐานตั้งแต่แรกแล้วคลาสที่ได้รับจะสืบทอดมาได้อย่างไร?


มิตรภาพแม้ว่าจะได้รับผ่านการประกาศเช่นเดียวกับสมาชิก แต่ก็ไม่ได้เป็นสมาชิกมากเท่ากับการแจ้งเตือนว่าชั้นเรียนอื่น ๆ สามารถเพิกเฉยต่อการจำแนกประเภทการเปิดเผยของสมาชิก 'ตัวจริง' ได้ ในขณะที่ส่วนข้อมูลจำเพาะที่คุณอ้างถึงจะอธิบายวิธีการทำงานของภาษาเกี่ยวกับความแตกต่างและพฤติกรรมของเฟรมเหล่านี้ในคำศัพท์ที่สอดคล้องกับตัวเอง แต่สิ่งต่าง ๆ อาจถูกสร้างขึ้นมาแตกต่างกันไปและไม่มีสิ่งใดที่จะเป็นหัวใจสำคัญของเหตุผลได้
Jeff

0

ฟังก์ชัน Friend ในคลาสจะกำหนดคุณสมบัติภายนอกให้กับฟังก์ชัน กล่าวคือภายนอกหมายความว่าฟังก์ชันได้รับการประกาศและกำหนดไว้ที่ใดที่หนึ่งจากชั้นเรียน

ดังนั้นจึงหมายความว่าฟังก์ชันเพื่อนไม่ใช่สมาชิกของชั้นเรียน ดังนั้นการสืบทอดจึงอนุญาตให้คุณสืบทอดคุณสมบัติของคลาสเท่านั้นไม่ใช่สิ่งภายนอก และหากอนุญาตให้มีการสืบทอดสำหรับฟังก์ชันของเพื่อนคลาสของบุคคลที่สามจะสืบทอด


0

เพื่อนเป็นสิ่งที่ดีในการสืบทอดเช่นอินเทอร์เฟซสไตล์สำหรับคอนเทนเนอร์ แต่สำหรับฉันอย่างแรกพูด 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
    }
};

สำหรับฉันปัญหาของ C ++ คือการขาดรายละเอียดที่ดีมากในการควบคุมการเข้าถึงทั้งหมดจากทุกที่:

เพื่อน Thing สามารถกลายเป็นเพื่อน Thing ได้ * เพื่อให้สิทธิ์เข้าถึงบุตรหลานของ Thing ทั้งหมด

และอื่น ๆ เพื่อน [ชื่อพื้นที่] สิ่ง * เพื่อให้สิทธิ์การเข้าถึงที่แม่นยำอยู่ในคลาสคอนเทนเนอร์ผ่านพื้นที่ที่มีชื่อพิเศษสำหรับเพื่อน

โอเคหยุดความฝัน แต่ตอนนี้คุณรู้จักเพื่อนที่น่าสนใจแล้ว

ในอีกลำดับหนึ่งคุณสามารถพบว่าทุกชั้นเรียนมีความเป็นมิตรกับตนเอง กล่าว
อีกนัยหนึ่งอินสแตนซ์คลาสสามารถเรียกสมาชิกทั้งหมดของอินสแตนซ์อื่นที่มีชื่อเดียวกันได้โดยไม่มีข้อ จำกัด :

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

0

ตรรกะง่ายๆ: 'ฉันมีเจนเพื่อนคนหนึ่ง เพียงเพราะเรากลายเป็นเพื่อนกันเมื่อวานนี้ไม่ได้ทำให้เพื่อนของเธอทั้งหมดเป็นของฉัน '

ฉันยังคงต้องเห็นด้วยกับมิตรภาพของแต่ละคนและระดับความไว้วางใจก็จะเป็นไปตามนั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.