คอนสตรัคเตอร์ส่วนตัวไม่ใช่คอนสตรัคเตอร์ส่วนตัวเมื่อใด


92

สมมติว่าฉันมีประเภทและฉันต้องการทำให้ตัวสร้างเริ่มต้นเป็นแบบส่วนตัว ฉันเขียนสิ่งต่อไปนี้:

class C {
    C() = default;
};

int main() {
    C c;           // error: C::C() is private within this context (g++)
                   // error: calling a private constructor of class 'C' (clang++)
                   // error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
    auto c2 = C(); // error: as above
}

เยี่ยมมาก

แต่แล้วคอนสตรัคเตอร์กลับกลายเป็นว่าไม่เป็นส่วนตัวเท่าที่ฉันคิดไว้:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

สิ่งนี้ทำให้ฉันรู้สึกประหลาดใจไม่คาดคิดและไม่ต้องการอย่างชัดเจน ทำไมถึงตกลง?


25
ไม่ใช่การC c{};เริ่มต้นรวมจึงไม่มีการเรียกตัวสร้าง?
NathanOliver

5
@NathanOliver พูดอะไร คุณไม่มีตัวสร้างที่ผู้ใช้จัดหาให้ดังนั้นจึงCเป็นการรวม
Kerrek SB

5
@KerrekSB ในเวลาเดียวกันฉันค่อนข้างแปลกใจที่ผู้ใช้ประกาศ ctor อย่างชัดเจนไม่ได้ทำให้ ctor นั้นให้ผู้ใช้
Angew ไม่ภูมิใจใน SO

1
@Angew นั่นคือเหตุผลที่เราทุกคนอยู่ที่นี่ :)
Barry

2
@Angew ถ้าเป็น=defaultctor สาธารณะนั่นจะดูสมเหตุสมผลกว่า แต่=defaultctor ส่วนตัวดูเหมือนเป็นสิ่งสำคัญที่ไม่ควรละเลย ยิ่งไปกว่านั้นclass C { C(); } inline C::C()=default;ความแตกต่างค่อนข้างน่าแปลกใจ
Yakk - Adam Nevraumont

คำตอบ:


61

เคล็ดลับอยู่ใน C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:

... ฟังก์ชันนี้มีให้โดยผู้ใช้หากมีการประกาศโดยผู้ใช้และไม่ได้ผิดนัดหรือถูกลบอย่างชัดเจนในการประกาศครั้งแรก ...

ซึ่งหมายความว่าคอนCสตรัคเตอร์เริ่มต้นไม่ได้ให้โดยผู้ใช้เนื่องจากเป็นค่าเริ่มต้นอย่างชัดเจนในการประกาศครั้งแรก ด้วยเหตุนี้Cจึงไม่มีตัวสร้างที่ผู้ใช้จัดหาให้ดังนั้นจึงเป็นการรวมต่อ 8.5.1 / 1 [dcl.init.aggr]:

การรวมคืออาร์เรย์หรือคลาส (ข้อ 9) ที่ไม่มีคอนสตรัคเตอร์ที่ผู้ใช้จัดหาให้ (12.1) ไม่มีสมาชิกข้อมูลส่วนตัวหรือไม่คงที่ที่มีการป้องกัน (ข้อ 11) ไม่มีคลาสพื้นฐาน (ข้อ 10) และไม่มีฟังก์ชันเสมือน (10.3 ).


13
ข้อบกพร่องมาตรฐานเล็ก ๆ : ความจริงที่ว่า ctor เริ่มต้นเป็นไพรเวตจะถูกละเว้นในบริบทนี้
Yakk - Adam Nevraumont

2
@ ยัคฉันไม่รู้สึกว่ามีคุณสมบัติพอที่จะตัดสินสิ่งนั้น แม้ว่าคำพูดเกี่ยวกับ ctor ที่ไม่ได้ให้โดยผู้ใช้นั้นดูเป็นการจงใจ
Angew ไม่ภูมิใจใน SO

1
@ ยัก: ใช่และไม่ใช่ หากชั้นเรียนมีสมาชิกข้อมูลคุณจะมีโอกาสทำให้ข้อมูลเหล่านั้นเป็นส่วนตัว หากไม่มีสมาชิกข้อมูลมีสถานการณ์น้อยมากที่สถานการณ์นี้จะส่งผลกระทบต่อทุกคนอย่างจริงจัง
Kerrek SB

2
@KerrekSB เป็นเรื่องสำคัญถ้าคุณพยายามใช้คลาสประเภทของ "โทเค็นการเข้าถึง" การควบคุมเช่นผู้ที่สามารถเรียกใช้ฟังก์ชันโดยพิจารณาจากผู้ที่สามารถสร้างวัตถุของคลาสได้
Angew ไม่ภูมิใจใน SO

5
@Yakk สิ่งที่น่าสนใจยิ่งกว่าคือมันใช้C{}งานได้แม้ว่าตัวสร้างจะเป็นdeleted ก็ตาม
Barry

56

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

จาก[dcl.init.aggr] / 1 :

การรวมคืออาร์เรย์หรือคลาส (Clause [class]) ที่มี

  • ไม่มีตัวสร้างที่ผู้ใช้ระบุ ([class.ctor]) (รวมถึงสิ่งที่สืบทอด ([namespace.udecl]) จากคลาสพื้นฐาน)
  • ไม่มีสมาชิกข้อมูลที่เป็นส่วนตัวหรือได้รับการป้องกัน (Clause [class.access])
  • ไม่มีฟังก์ชันเสมือน ([class.virtual]) และ
  • ไม่มีคลาสพื้นฐานเสมือนส่วนตัวหรือที่มีการป้องกัน ([class.mi])

และจาก[dcl.fct.def.default] / 5

ฟังก์ชันที่เป็นค่าเริ่มต้นอย่างชัดเจนและฟังก์ชันที่ประกาศโดยนัยเรียกรวมกันว่าฟังก์ชันเริ่มต้นและการนำไปใช้งานจะต้องให้คำจำกัดความโดยนัยสำหรับฟังก์ชันเหล่านี้ ([class.ctor] [class.dtor], [class.copy]) ซึ่งอาจหมายถึงการกำหนดให้เป็นลบ . ฟังก์ชันนี้มีให้โดยผู้ใช้หากมีการประกาศโดยผู้ใช้และไม่ได้ตั้งค่าเริ่มต้นหรือลบออกอย่างชัดเจนในการประกาศครั้งแรก ฟังก์ชันที่เป็นค่าเริ่มต้นอย่างชัดเจนที่ผู้ใช้ระบุ (กล่าวคือเริ่มต้นอย่างชัดเจนหลังจากการประกาศครั้งแรก) ถูกกำหนดไว้ ณ จุดที่เป็นค่าเริ่มต้นอย่างชัดเจน หากฟังก์ชันดังกล่าวถูกกำหนดโดยปริยายว่าถูกลบแสดงว่าโปรแกรมนั้นมีรูปแบบที่ไม่ถูกต้อง[หมายเหตุ: การประกาศฟังก์ชันเป็นค่าเริ่มต้นหลังจากการประกาศครั้งแรกสามารถให้การดำเนินการที่มีประสิทธิภาพและคำจำกัดความที่กระชับในขณะที่เปิดใช้งานส่วนต่อประสานไบนารีที่เสถียรให้กับฐานรหัสที่กำลังพัฒนา - หมายเหตุ]

ดังนั้นข้อกำหนดของเราสำหรับการรวมคือ:

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

C เป็นไปตามข้อกำหนดเหล่านี้ทั้งหมด

โดยปกติแล้วคุณสามารถกำจัดพฤติกรรมการก่อสร้างเริ่มต้นที่ผิดพลาดนี้ได้โดยเพียงแค่ระบุตัวสร้างเริ่มต้นที่ว่างเปล่าหรือโดยการกำหนดตัวสร้างเป็นค่าเริ่มต้นหลังจากประกาศ:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;

2
ฉันชอบคำตอบนี้ค่อนข้างดีกว่าคำตอบของ Angew แต่ฉันคิดว่ามันจะได้ประโยชน์จากการสรุปในตอนต้นไม่เกินสองประโยค
PJTraill

7

ของ AngewและjaggedSpire ของคำตอบที่ดีและนำไปใช้. และ. และ.

อย่างไรก็ตามใน สิ่งต่างๆเปลี่ยนไปเล็กน้อยและตัวอย่างใน OP จะไม่รวบรวมอีกต่อไป:

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

ตามที่ระบุไว้ในคำตอบสองข้อเหตุผลที่การประกาศสองครั้งหลังใช้ได้ผลเพราะCเป็นการรวมและนี่คือการเริ่มต้นรวม อย่างไรก็ตามจากผลของP1008 (โดยใช้ตัวอย่างที่จูงใจไม่ให้แตกต่างจาก OP มากเกินไป) คำจำกัดความของการเปลี่ยนแปลงโดยรวมใน C ++ 20 ถึงจาก[dcl.init.aggr] / 1 :

การรวมคืออาร์เรย์หรือคลาส ([คลาส]) ที่มี

  • ไม่มีตัวสร้างที่ผู้ใช้ประกาศหรือสืบทอด ([class.ctor])
  • ไม่มีสมาชิกข้อมูลโดยตรงที่เป็นส่วนตัวหรือได้รับการป้องกันโดยตรง ([class.access])
  • ไม่มีฟังก์ชันเสมือน ([class.virtual]) และ
  • ไม่มีคลาสพื้นฐานเสมือนส่วนตัวหรือที่มีการป้องกัน ([class.mi])

เน้นของฉัน ตอนนี้ความต้องการที่จะไม่ใช้การประกาศการก่อสร้างในขณะที่มันเคยเป็น (เป็นทั้งผู้ใช้อ้างอิงในคำตอบของพวกเขาและสามารถดูได้ในอดีตสำหรับC ++ 11 , C ++ 14และC ++ 17 ) ไม่มีผู้ใช้ให้ก่อสร้าง . ตัวสร้างเริ่มต้นสำหรับCคือผู้ใช้ประกาศ แต่ไม่ได้ให้โดยผู้ใช้และด้วยเหตุนี้จึงสิ้นสุดการรวมใน C ++ 20


นี่คืออีกตัวอย่างที่แสดงให้เห็นถึงการเปลี่ยนแปลงโดยรวม:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

Bไม่ใช่การรวมใน C ++ 11 หรือ C ++ 14 เนื่องจากมีคลาสพื้นฐาน ด้วยเหตุนี้B{}เพียงแค่เรียกใช้ตัวสร้างเริ่มต้น (ประกาศโดยผู้ใช้ แต่ไม่ได้ให้โดยผู้ใช้) ซึ่งสามารถเข้าถึงตัวAสร้างเริ่มต้นที่ได้รับการป้องกัน

ใน C ++ 17 อันเป็นผลมาจากP0017 การรวมถูกขยายเพื่ออนุญาตให้มีคลาสพื้นฐาน Bคือการรวมใน C ++ 17 ซึ่งหมายความว่าB{}เป็นการเริ่มต้นรวมที่ต้องเตรียมใช้งานวัตถุย่อยทั้งหมดรวมถึงAวัตถุย่อย แต่เนื่องจากตัวAสร้างเริ่มต้นได้รับการป้องกันเราจึงไม่สามารถเข้าถึงได้ดังนั้นการเริ่มต้นนี้จึงมีรูปแบบที่ไม่ถูกต้อง

ใน C ++ 20 เนื่องจากคอนBสตรัคเตอร์ที่ผู้ใช้ประกาศไว้มันจึงยุติการรวมอีกครั้งดังนั้นจึงเปลี่ยนB{}กลับเป็นการเรียกใช้คอนสตรัคเตอร์เริ่มต้นและนี่คือการเริ่มต้นที่มีรูปแบบที่ดีอีกครั้ง

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