เหตุใดจึงไม่เรียกเมธอด public const เมื่อ non-const เป็นแบบส่วนตัว


117

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

struct A
{
    void foo() const
    {
        std::cout << "const" << std::endl;
    }

    private:

        void foo()
        {
            std::cout << "non - const" << std::endl;
        }
};

int main()
{
    A a;
    a.foo();
}

ข้อผิดพลาดของคอมไพเลอร์คือ:

ข้อผิดพลาด: 'void A :: foo ()' is private`

แต่เมื่อฉันลบไพรเวตมันก็ใช้ได้ เหตุใดจึงไม่เรียกเมธอด public const เมื่อ non-const เป็นแบบส่วนตัว

กล่าวอีกนัยหนึ่งทำไมความละเอียดเกินมาก่อนการควบคุมการเข้าถึง มันแปลก ๆ. คิดว่าสอดคล้องกันไหม? รหัสของฉันใช้งานได้จากนั้นฉันก็เพิ่มวิธีการและรหัสการทำงานของฉันไม่ได้รวบรวมเลย


3
ใน C ++ โดยไม่ต้องใช้ความพยายามเป็นพิเศษเช่นการใช้สำนวน PIMPL ไม่มีส่วน "ส่วนตัว" ที่แท้จริงของคลาส นี่เป็นเพียงหนึ่งในปัญหา (การเพิ่มเมธอด "ส่วนตัว" มากเกินไปและทำลายโค้ดเก่าที่รวบรวมไว้จะนับเป็นปัญหาในหนังสือของฉันแม้ว่าอันนี้จะเป็นเรื่องเล็กน้อยที่ต้องหลีกเลี่ยงโดยไม่ได้ทำก็ตาม) ที่เกิดจากมัน
hyde

มีรหัสในชีวิตจริงที่คุณคาดว่าจะสามารถเรียกใช้ฟังก์ชัน const ได้หรือไม่ แต่คู่ที่ไม่ใช่ const จะเป็นส่วนหนึ่งของอินเทอร์เฟซส่วนตัว ดูเหมือนว่าการออกแบบส่วนต่อประสานที่ไม่ดีสำหรับฉัน
Vincent Fourmond

คำตอบ:


125

เมื่อคุณเรียกa.foo();คอมไพเลอร์จะผ่านความละเอียดเกินพิกัดเพื่อค้นหาฟังก์ชันที่ดีที่สุดที่จะใช้ เมื่อสร้างชุดโอเวอร์โหลดจะพบ

void foo() const

และ

void foo()

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

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

กล่าวอีกนัยหนึ่งว่าทำไมความละเอียดเกินมาก่อนการควบคุมการเข้าถึง?

ลองดูที่:

struct Base
{
    void foo() { std::cout << "Base\n"; }
};

struct Derived : Base
{
    void foo() { std::cout << "Derived\n"; }
};

struct Foo
{
    void foo(Base * b) { b->foo(); }
private:
    void foo(Derived * d) { d->foo(); }
};

int main()
{
    Derived d;
    Foo f;
    f.foo(&d);
}

ตอนนี้สมมติว่าฉันไม่ได้ตั้งใจจะทำให้เป็นvoid foo(Derived * d)ส่วนตัว หากการควบคุมการเข้าถึงมาก่อนโปรแกรมนี้จะคอมไพล์และรันและBaseจะพิมพ์ออกมา อาจเป็นเรื่องยากมากที่จะติดตามในฐานรหัสขนาดใหญ่ เนื่องจากการควบคุมการเข้าถึงเกิดขึ้นหลังจากความละเอียดเกินพิกัดฉันจึงได้รับข้อผิดพลาดของคอมไพเลอร์ที่บอกฉันว่าฟังก์ชั่นที่ฉันต้องการให้เรียกไม่สามารถเรียกได้และฉันสามารถค้นหาข้อบกพร่องได้ง่ายขึ้นมาก


มีสาเหตุใดบ้างที่ทำให้การควบคุมการเข้าถึงอยู่หลังจากความละเอียดเกินพิกัด
drake7707

3
@ drake7707 Wll ตามที่ฉันแสดงในตัวอย่างโค้ดของฉันคุณถ้าการควบคุมการเข้าถึงมาก่อนโค้ดด้านบนจะคอมไพล์ซึ่งจะเปลี่ยนความหมายของโปรแกรม ไม่แน่ใจเกี่ยวกับคุณ แต่ฉันค่อนข้างจะมีข้อผิดพลาดและจำเป็นต้องทำการแคสต์อย่างชัดเจนหากฉันต้องการให้ฟังก์ชันยังคงเป็นส่วนตัวจากนั้นการแคสต์โดยปริยายและโค้ดจะ "ทำงาน" อย่างเงียบ ๆ
NathanOliver

"และจำเป็นต้องทำการแคสต์อย่างชัดเจนหากฉันต้องการให้ฟังก์ชันยังคงเป็นส่วนตัว" - ดูเหมือนว่าปัญหาที่แท้จริงในที่นี้คือการร่ายโดยปริยาย ... แม้ว่าในทางกลับกันความคิดที่ว่าคุณสามารถใช้คลาสที่ได้รับมาโดยปริยายโดยปริยายเป็น คลาสฐานเป็นลักษณะที่กำหนดของกระบวนทัศน์ OO ใช่หรือไม่?
Steven Byks

35

ในท้ายที่สุดนี้ลงมาเพื่อยืนยันในมาตรฐานที่การเข้าถึงไม่ควรจะนำมาพิจารณาเมื่อดำเนินการมติเกินพิกัด การยืนยันนี้สามารถพบได้ใน[over.match]ข้อ 3:

... เมื่อการแก้ปัญหาโอเวอร์โหลดสำเร็จและไม่สามารถเข้าถึงฟังก์ชันที่ทำงานได้ดีที่สุด (Clause [class.access]) ในบริบทที่ใช้โปรแกรมจะมีรูปแบบไม่ถูกต้อง

และหมายเหตุในข้อ 1 ของส่วนเดียวกัน:

[หมายเหตุ: ไม่รับประกันว่าฟังก์ชันที่เลือกโดยความละเอียดเกินพิกัดจะเหมาะสมกับบริบท ข้อ จำกัด อื่น ๆ เช่นความสามารถในการเข้าถึงของฟังก์ชันสามารถใช้งานได้ในบริบทการโทรที่ไม่เหมาะสม - หมายเหตุ]

เหตุใดฉันจึงนึกถึงแรงจูงใจที่เป็นไปได้สองประการ:

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

32

สมมติว่าการควบคุมการเข้าถึงมาก่อนความละเอียดเกินพิกัด อย่างมีประสิทธิภาพนี่จะหมายถึงpublic/protected/privateการมองเห็นที่ควบคุมได้มากกว่าการเข้าถึง

ส่วนที่ 2.10 ของการออกแบบและวิวัฒนาการของ C ++ โดย Stroustrupมีเนื้อหาเกี่ยวกับเรื่องนี้ซึ่งเขากล่าวถึงตัวอย่างต่อไปนี้

int a; // global a

class X {
private:
    int a; // member X::a
};

class XX : public X {
    void f() { a = 1; } // which a?
};

Stroustrup กล่าวว่าประโยชน์ของกฎปัจจุบัน (การมองเห็นก่อนการเข้าถึง) คือ (ชั่วคราว) การเปลี่ยนprivateภายในclass Xเป็นpublic(เช่นเพื่อวัตถุประสงค์ในการดีบัก) คือไม่มีการเปลี่ยนแปลงอย่างเงียบ ๆ ในความหมายของโปรแกรมข้างต้น (เช่นX::aคือพยายามที่จะ เข้าถึงได้ในทั้งสองกรณีซึ่งทำให้เกิดข้อผิดพลาดในการเข้าถึงในตัวอย่างด้านบน) หากpublic/protected/privateจะควบคุมการมองเห็นความหมายของโปรแกรมจะเปลี่ยนไป (global aจะเรียกด้วยprivateมิฉะนั้นX::a )

จากนั้นเขาก็บอกว่าเขาจำไม่ได้ว่ามันเกิดจากการออกแบบที่ชัดเจนหรือผลข้างเคียงของเทคโนโลยีพรีโปรเซสเซอร์ที่ใช้ในการนำ C ที่มี Classess รุ่นก่อนไปใช้กับ Standard C ++

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

10.2 การค้นหาชื่อสมาชิก [class.member.lookup]

1 การค้นหาชื่อสมาชิกกำหนดความหมายของชื่อ (id-expression) ในขอบเขตคลาส (3.3.7) การค้นหาชื่ออาจทำให้เกิดความคลุมเครือซึ่งในกรณีนี้โปรแกรมมีรูปแบบไม่ถูกต้อง สำหรับนิพจน์ id การค้นหาชื่อจะเริ่มต้นในขอบเขตคลาสของสิ่งนี้ สำหรับ ID ที่ผ่านการรับรองการค้นหาชื่อจะเริ่มต้นในขอบเขตของตัวระบุชื่อที่ซ้อนกัน การค้นหาชื่อจะเกิดขึ้นก่อนการควบคุมการเข้าถึง (3.4, ข้อ 11)

8 หากพบชื่อของฟังก์ชันที่โอเวอร์โหลดอย่างไม่น่าสงสัย ความละเอียดเกินพิกัด (13.3) จะเกิดขึ้นก่อนการควบคุมการเข้าถึงด้วย ความคลุมเครือสามารถแก้ไขได้โดยการกำหนดชื่อด้วยชื่อคลาส


23

เนื่องจากthisตัวชี้โดยนัยไม่ใช่ - constก่อนอื่นคอมไพเลอร์จะตรวจสอบการมีอยู่ของconstฟังก์ชันที่ไม่ใช่เวอร์ชันก่อนconstเวอร์ชัน

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


คิดว่าสอดคล้องกันไหม? รหัสของฉันใช้งานได้จากนั้นฉันเพิ่มวิธีการและรหัสการทำงานของฉันไม่รวบรวมเลย
Narek

ฉันคิดอย่างนั้น ความละเอียดเกินพิกัดเป็นเรื่องยุ่งยากโดยเจตนา ฉันตอบคำถามที่คล้ายกันเมื่อวานนี้: stackoverflow.com/questions/39023325/…
Bathsheba

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

3
@Narek ก่อนอื่นฉันสงสัยว่าทำไมมันถึงใช้งานไม่ได้ แต่ลองพิจารณาสิ่งนี้: คุณจะเรียกใช้ฟังก์ชันส่วนตัวได้อย่างไรหากควรเลือก const สาธารณะสำหรับวัตถุที่ไม่ใช่ const ด้วย
idclev 463035818

20

สิ่งสำคัญคือต้องคำนึงถึงลำดับของสิ่งที่เกิดขึ้นซึ่งก็คือ:

  1. ค้นหาฟังก์ชันที่ใช้งานได้ทั้งหมด
  2. เลือกฟังก์ชันที่ทำงานได้ดีที่สุด
  3. หากไม่มีฟังก์ชันที่ดีที่สุดอย่างใดอย่างหนึ่งหรือถ้าคุณไม่สามารถเรียกใช้ฟังก์ชันที่ทำงานได้ดีที่สุด (เนื่องจากการละเมิดการเข้าถึงหรือฟังก์ชันเป็นdeleted) ล้มเหลว

(3) เกิดขึ้นหลังจาก (2) ซึ่งสำคัญมากเพราะมิฉะนั้นการสร้างฟังก์ชันdeleted หรือprivateจะกลายเป็นเรื่องที่ไร้ความหมายและยากที่จะให้เหตุผล

ในกรณีนี้:

  1. ฟังก์ชั่นที่ทำงานอยู่และA::foo()A::foo() const
  2. ฟังก์ชันที่ทำงานได้ดีที่สุดคือA::foo()เนื่องจากส่วนหลังเกี่ยวข้องกับการแปลงคุณสมบัติในthisอาร์กิวเมนต์โดยปริยาย
  3. แต่A::foo()เป็นprivateและคุณไม่สามารถเข้าถึงได้ดังนั้นรหัสจึงไม่ถูกต้อง

1
อาจมีคนคิดว่า "ทำงานได้" จะรวมถึงข้อ จำกัด การเข้าถึงที่เกี่ยวข้อง กล่าวอีกนัยหนึ่งคือ "ทำงานได้" ที่จะเรียกใช้ฟังก์ชันส่วนตัวจากภายนอกคลาสไม่ได้เนื่องจากไม่ได้เป็นส่วนหนึ่งของอินเทอร์เฟซสาธารณะของคลาสนั้น
RM

15

สิ่งนี้ถือเป็นการตัดสินใจออกแบบขั้นพื้นฐานใน C ++

เมื่อค้นหาฟังก์ชันเพื่อตอบสนองการโทรคอมไพลเลอร์จะทำการค้นหาดังนี้:

  1. มันค้นหาเพื่อหาสิ่งที่แรกที่1ขอบเขตที่มีบางสิ่งบางอย่างที่มีชื่อนั้น

  2. คอมไพเลอร์ค้นหาฟังก์ชันทั้งหมด (หรือ functors ฯลฯ ) ที่มีชื่อนั้นในขอบเขตนั้น

  3. จากนั้นคอมไพเลอร์จะทำการแก้ปัญหาโอเวอร์โหลดเพื่อค้นหาตัวเลือกที่ดีที่สุดในบรรดาที่พบ (ไม่ว่าจะเข้าถึงได้หรือไม่ก็ตาม)

  4. สุดท้ายคอมไพลเลอร์จะตรวจสอบว่าฟังก์ชันที่เลือกนั้นสามารถเข้าถึงได้หรือไม่

เนื่องจากคำสั่งดังกล่าวใช่เป็นไปได้ที่คอมไพเลอร์จะเลือกโอเวอร์โหลดที่ไม่สามารถเข้าถึงได้แม้ว่าจะมีโอเวอร์โหลดอื่นที่สามารถเข้าถึงได้ (แต่ไม่ได้เลือกระหว่างการแก้ปัญหาโอเวอร์โหลด)

ว่าจะเป็นไปได้หรือไม่ที่จะทำสิ่งต่างๆที่แตกต่างออกไปใช่มันเป็นไปได้อย่างไม่ต้องสงสัย แน่นอนว่าจะนำไปสู่ภาษาที่แตกต่างจาก C ++ ปรากฎว่าการตัดสินใจที่ดูเหมือนค่อนข้างเล็กน้อยอาจมีผลกระทบที่ส่งผลกระทบมากกว่าที่เห็นได้ชัดในตอนแรก


  1. "ขั้นแรก" อาจมีความซับซ้อนเล็กน้อยในตัวมันเองโดยเฉพาะอย่างยิ่งเมื่อ / ถ้าเทมเพลตเข้ามาเกี่ยวข้องเนื่องจากสามารถนำไปสู่การค้นหาแบบสองเฟสซึ่งหมายความว่าจะมี "ราก" สองรายการที่แยกจากกันโดยสิ้นเชิงเพื่อเริ่มต้นเมื่อทำการค้นหา พื้นฐานความคิดที่สวยเรียบง่ายแม้ว่า: เริ่มต้นจากการปิดล้อมขอบเขตที่เล็กที่สุดและวิธีการทำงานของคุณออกไปด้านนอกจะมีขนาดใหญ่และมีขนาดใหญ่ล้อมรอบขอบเขต

1
Stroustrup คาดเดาใน D&E ว่ากฎอาจเป็นผลข้างเคียงของตัวประมวลผลล่วงหน้าที่ใช้ใน C กับคลาสที่ไม่เคยได้รับการตรวจสอบเลยเมื่อมีเทคโนโลยีคอมไพเลอร์ขั้นสูงอีกแล้ว ดูคำตอบของฉัน
TemplateRex

12

การควบคุมการเข้าถึง ( public, protected, private) ไม่ได้ส่งผลกระทบต่อการโอเวอร์โหลดความละเอียด คอมไพเลอร์เลือกvoid foo()เพราะตรงกับที่สุด ความจริงที่ว่ามันไม่สามารถเข้าถึงได้ไม่ได้เปลี่ยนไป การลบออกเท่านั้นvoid foo() constซึ่งจะเป็นการจับคู่ที่ดีที่สุด (กล่าวคือเท่านั้น)


11

ในสายนี้:

a.foo();

มีthisตัวชี้โดยปริยายอยู่เสมอในทุกฟังก์ชันของสมาชิก และconstคุณสมบัติของthisจะนำมาจากการอ้างอิงการเรียก / วัตถุ การเรียกข้างต้นได้รับการปฏิบัติโดยคอมไพเลอร์เป็น:

A::foo(a);

แต่คุณมีคำประกาศสองข้อA::fooคือได้รับการปฏิบัติดังนี้:

A::foo(A* );
A::foo(A const* );

โดยความละเอียดเกินพิกัดรายการแรกจะถูกเลือกสำหรับ non-const thisส่วนที่สองจะถูกเลือกสำหรับไฟล์const this . หากคุณลบครั้งแรกที่สองจะผูกกับทั้งสองและconstnon-const this

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

มาตรฐานกล่าวว่า:

[class.access / 4] : ... ในกรณีของชื่อฟังก์ชันที่โอเวอร์โหลดการควบคุมการเข้าถึงจะใช้กับฟังก์ชันที่เลือกโดยความละเอียดเกิน ....

แต่ถ้าคุณทำสิ่งนี้:

A a;
const A& ac = a;
ac.foo();

จากนั้นเฉพาะconstโอเวอร์โหลดเท่านั้นที่จะพอดี


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

@Narek, .. ฉันได้อัปเดตคำตอบของฉันโดยอ้างอิงถึงมาตรฐาน C ++ มันสมเหตุสมผลอย่างนั้นมีหลายสิ่งหลายอย่างและสำนวนใน C ++ ที่ขึ้นอยู่กับพฤติกรรมนี้
WhiZTiM

9

เหตุผลทางเทคนิคได้รับคำตอบจากคำตอบอื่น ๆ ฉันจะเน้นเฉพาะคำถามนี้:

กล่าวอีกนัยหนึ่งว่าทำไมความละเอียดเกินมาก่อนการควบคุมการเข้าถึง? มันแปลก ๆ. คิดว่าสอดคล้องกันไหม? รหัสของฉันใช้งานได้จากนั้นฉันเพิ่มวิธีการและรหัสการทำงานของฉันไม่รวบรวมเลย

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

ในทางกลับกันสมมติว่าโค้ดของคุณคอมไพล์และทำงานได้ดีกับconstฟังก์ชันสมาชิกที่เรียกใช้ สักวันหนึ่งคน (อาจจะด้วยตัวคุณเอง) จึงตัดสินใจที่จะเปลี่ยนการเข้าถึงของที่ไม่ใช่constฟังก์ชันสมาชิกจากการprivate publicจากนั้นพฤติกรรมจะเปลี่ยนไปโดยไม่มีข้อผิดพลาดในการคอมไพล์! นี้จะเป็นที่น่าแปลกใจ



8

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

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

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