เหตุใดฟังก์ชันที่แทนที่ในคลาสที่ได้รับจะซ่อนโอเวอร์โหลดอื่น ๆ ของคลาสพื้นฐาน


220

พิจารณารหัส:

#include <stdio.h>

class Base {
public: 
    virtual void gogo(int a){
        printf(" Base :: gogo (int) \n");
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*) \n");
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*) \n");
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

มีข้อผิดพลาดนี้:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: ในฟังก์ชัน `int main () ':
test.cpp: 31: ข้อผิดพลาด: ไม่มีฟังก์ชั่นที่ตรงกันสำหรับการโทรไปที่ `Derived :: gogo (int) '
test.cpp: 21: หมายเหตุ: ผู้สมัครคือ: void เสมือน Derived :: gogo (int *) 
test.cpp: 33: 2: คำเตือน: ไม่มีบรรทัดใหม่ที่ท้ายไฟล์
> รหัสทางออก: 1

ที่นี่ฟังก์ชั่นของชั้นที่ได้รับมาคือ eclipsing ฟังก์ชั่นทั้งหมดที่มีชื่อเดียวกัน ยังไงก็ตามพฤติกรรมของ C ++ นี้ดูไม่เป็นไร ไม่ใช่ polymorphic



8
คำถามที่ยอดเยี่ยมฉันเพิ่งค้นพบสิ่งนี้เมื่อเร็ว ๆ นี้เช่นกัน
Matt Joiner

11
ฉันคิดว่า Bjarne (จากลิงก์ของ Mac โพสต์) ทำให้ดีที่สุดในประโยคเดียว: "ใน C ++ ไม่มีการโหลดเกินขอบเขต - ขอบเขตคลาสที่ได้รับไม่ใช่ข้อยกเว้นของกฎทั่วไปนี้"
sivabudh

7
@Aishish ลิงก์นั้นใช้งานไม่ได้ นี่คือหนึ่งที่ถูกต้อง (ณ ตอนนี้) - stroustrup.com/bs_faq2.html#overloadderived
nsane

3
นอกจากนี้ต้องการชี้ให้เห็นว่าobj.Base::gogo(7);ยังคงทำงานได้โดยการเรียกฟังก์ชั่นที่ซ่อนอยู่
forumulator

คำตอบ:


406

ตัดสินจากถ้อยคำของคำถามของคุณ (คุณใช้คำว่า "ซ่อน") คุณรู้อยู่แล้วว่าเกิดอะไรขึ้นที่นี่ ปรากฏการณ์นี้เรียกว่า "การซ่อนชื่อ" ด้วยเหตุผลบางอย่างทุกครั้งที่มีคนถามคำถามว่าทำไมการซ่อนชื่อเกิดขึ้นผู้ที่ตอบว่าอาจเรียกว่า "การซ่อนชื่อ" และอธิบายวิธีการใช้งาน (ซึ่งคุณอาจรู้อยู่แล้ว) หรืออธิบายวิธีแทนที่มัน ไม่เคยถาม) แต่ดูเหมือนว่าไม่มีใครสนใจที่จะตอบคำถาม "ทำไม" จริง

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

ตัวอย่างเช่นสมมติว่าชั้นฐานBมีฟังก์ชั่นสมาชิกfooที่ใช้พารามิเตอร์ของประเภทvoid *และทุกสายที่จะได้รับการแก้ไขไปfoo(NULL) B::foo(void *)สมมติว่าไม่มีที่หลบซ่อนชื่อและที่นี้ปรากฏอยู่ในชั้นเรียนที่แตกต่างกันลงมาจากB::foo(void *) Bแต่ขอบอกว่าในบาง [อ้อมระยะไกล] ลูกหลานDของชั้นBฟังก์ชั่นfoo(int)ที่มีการกำหนด ตอนนี้ไม่มีการซ่อนชื่อDมีทั้งfoo(void *)และfoo(int)มองเห็นได้และมีส่วนร่วมในการแก้ปัญหาเกินพิกัด ฟังก์ชั่นใดจะเรียกการfoo(NULL)แก้ไขถ้าทำผ่านวัตถุประเภทD? พวกเขาจะแก้ปัญหาD::foo(int)เนื่องจากintเป็นการจับคู่ที่ดีกว่าสำหรับศูนย์รวม (เช่นNULL) กว่าตัวชี้ประเภทใดก็ได้ ดังนั้นตลอดลำดับชั้นเรียกร้องให้foo(NULL)แก้ปัญหาหนึ่งฟังก์ชั่นในขณะที่อยู่ในD(และภายใต้) พวกเขาก็แก้ไขไปยังอีก

ตัวอย่างอื่นได้รับในการออกแบบและวิวัฒนาการของ C ++หน้า 77:

class Base {
    int x;
public:
    virtual void copy(Base* p) { x = p-> x; }
};

class Derived{
    int xx;
public:
    virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copy Base part of b
    b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}

หากไม่มีกฎนี้สถานะของ b จะถูกอัปเดตบางส่วนซึ่งนำไปสู่การแบ่งส่วน

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

ในขณะที่คุณสังเกตเห็นอย่างถูกต้องในโพสต์ต้นฉบับของคุณ (ฉันหมายถึงคำพูด "Not polymorphic") พฤติกรรมนี้อาจถูกมองว่าเป็นการละเมิดความสัมพันธ์ระหว่าง IS-A ระหว่างชั้นเรียน เรื่องนี้เป็นเรื่องจริง แต่เห็นได้ชัดว่าเมื่อก่อนตัดสินใจว่าการซ่อนชื่อท้ายจะพิสูจน์ได้ว่าเป็นความชั่วร้ายที่น้อยกว่า


22
ใช่นี่เป็นคำตอบที่แท้จริงของคำถาม ขอบคุณ. ฉันก็สงสัยเช่นกัน
Omnifarious

4
คำตอบที่ดี! นอกจากนี้ในทางปฏิบัติแล้วการรวบรวมอาจช้าลงมากหากการค้นหาชื่อต้องดำเนินไปจนสุดทางทุกครั้ง
Drew Hall

6
(คำตอบเดิมฉันรู้แล้ว) ตอนนี้nullptrฉันจะคัดค้านตัวอย่างของคุณโดยพูดว่า "ถ้าคุณต้องการเรียกvoid*รุ่นคุณควรใช้ประเภทตัวชี้" มีตัวอย่างที่แตกต่างกันซึ่งสิ่งนี้อาจไม่ดีหรือไม่?
GManNickG

3
การซ่อนชื่อไม่ได้เลวร้ายจริงๆ ความสัมพันธ์ "is-a" ยังคงมีอยู่และพร้อมใช้งานผ่านอินเทอร์เฟซพื้นฐาน ดังนั้นอาจd->foo()จะไม่ได้รับ "Is-a Base" แต่static_cast<Base*>(d)->foo() จะรวมถึงการจัดส่งแบบไดนามิก
Kerrek SB

12
คำตอบนี้ไม่ช่วยเหลือเนื่องจากตัวอย่างที่ให้มามีลักษณะเหมือนกันโดยมีหรือไม่มีการซ่อน: D :: foo (int) จะถูกเรียกอย่างใดอย่างหนึ่งเนื่องจากเป็นการจับคู่ที่ดีกว่าหรือเพราะซ่อน B: foo (int)
Richard Wolf

46

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

ในกรณีgogo(int*)นี้พบ (คนเดียว) ในขอบเขตคลาสที่ได้รับและเนื่องจากไม่มีการแปลงมาตรฐานจาก int เป็น int * การค้นหาจึงล้มเหลว

วิธีแก้ปัญหาคือการนำการประกาศพื้นฐานมาทางการประกาศที่ใช้ในคลาสที่ได้รับ:

using Base::gogo;

... จะอนุญาตให้กฎการค้นหาชื่อค้นหาผู้สมัครทั้งหมดและการแก้ปัญหาการโอเวอร์โหลดจะดำเนินการตามที่คุณคาดไว้


10
OP: "ทำไมฟังก์ชันที่ถูกแทนที่ในคลาสที่ได้รับนั้นซ่อนโอเวอร์โหลดอื่น ๆ ของคลาสพื้นฐาน" คำตอบนี้: "เพราะมันเป็นเช่นนั้น"
Richard Wolf

12

นี่คือ "โดยการออกแบบ" ในการแก้ปัญหาการโอเวอร์โหลด C ++ สำหรับวิธีการนี้จะทำงานดังต่อไปนี้

  • เริ่มต้นที่ประเภทของการอ้างอิงแล้วไปที่ประเภทฐานค้นหาประเภทแรกซึ่งมีวิธีการที่ชื่อว่า "gogo"
  • เมื่อพิจารณาเฉพาะเมธอดที่ชื่อ "gogo" ในประเภทนั้นจะพบโอเวอร์โหลดที่ตรงกัน

เนื่องจาก Derived ไม่มีฟังก์ชันที่ตรงกับชื่อ "gogo" การแก้ปัญหาโอเวอร์โหลดจึงล้มเหลว


2

การซ่อนชื่อทำให้เข้าใจได้เพราะป้องกันความไม่ชัดเจนในการจำแนกชื่อ

พิจารณารหัสนี้:

class Base
{
public:
    void func (float x) { ... }
}

class Derived: public Base
{
public:
    void func (double x) { ... }
}

Derived dobj;

ถ้าBase::func(float)ไม่ถูกซ่อนโดยDerived::func(double)ใน Derived เราจะเรียกใช้ฟังก์ชันคลาสพื้นฐานเมื่อโทรdobj.func(0.f)ถึงแม้ว่าการเลื่อนสามารถเลื่อนได้เป็นสองเท่า

การอ้างอิง: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

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