เหตุใด C ++ จึงต้องการคอนสตรัคเตอร์เริ่มต้นที่ผู้ใช้กำหนดเพื่อสร้างอ็อบเจกต์ const เริ่มต้น


99

มาตรฐาน C ++ (ส่วนที่ 8.5) กล่าวว่า:

หากโปรแกรมเรียกใช้การเริ่มต้นเริ่มต้นของอ็อบเจ็กต์ที่มีคุณสมบัติ const ชนิด T T จะเป็นประเภทคลาสที่มีตัวสร้างดีฟอลต์ที่ผู้ใช้ระบุ

ทำไม? ฉันนึกไม่ออกว่าทำไมต้องใช้ตัวสร้างที่ผู้ใช้จัดหามาในกรณีนี้

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}

2
ดูเหมือนจะไม่จำเป็นต้องใช้บรรทัดในตัวอย่างของคุณ (ดูideone.com/qqiXR ) เนื่องจากคุณประกาศ แต่ไม่ได้กำหนด / เริ่มต้นaแต่ gcc-4.3.4 ยอมรับแม้ในขณะที่คุณทำ (ดูideone.com/uHvFS )
Ray Toal

aตัวอย่างข้างต้นทั้งประกาศและกำหนด Comeau สร้างข้อผิดพลาด "ตัวแปร const" a "ต้องใช้ตัวเริ่มต้น - คลาส" A "ไม่มีตัวสร้างเริ่มต้นที่ประกาศไว้อย่างชัดเจน" หากบรรทัดถูกแสดงความคิดเห็น
Karu


4
สิ่งนี้ได้รับการแก้ไขใน C ++ 11 คุณสามารถเขียนได้const A a{}:)
Howard Lovatt

คำตอบ:


10

นี้ก็ถือว่าเป็นข้อบกพร่อง (เทียบกับทุกรุ่นมาตรฐาน) และมันก็แก้ไขได้โดยหลักคณะทำงาน (CWG) ข้อบกพร่อง 253 ถ้อยคำใหม่สำหรับสถานะมาตรฐานในhttp://eel.is/c++draft/dcl.init#7

ประเภทคลาส T เป็นค่าเริ่มต้นที่สร้างได้หากค่าเริ่มต้นเริ่มต้นของ T จะเรียกใช้ตัวสร้าง T ที่ผู้ใช้จัดหาให้ (ไม่ได้รับมาจากคลาสฐาน) หรือถ้า

  • สมาชิกข้อมูลที่ไม่ใช่ตัวแปรโดยตรงแต่ละตัว M ของ T มีตัวเริ่มต้นสมาชิกเริ่มต้นหรือถ้า M เป็นประเภทคลาส X (หรืออาร์เรย์ของมัน) X เป็นค่าเริ่มต้นที่สร้างได้
  • ถ้า T เป็นสหภาพที่มีสมาชิกข้อมูลที่ไม่คงที่อย่างน้อยหนึ่งสมาชิกตัวแปรหนึ่งตัวมีค่าเริ่มต้นสมาชิกเริ่มต้น
  • ถ้า T ไม่ใช่สหภาพสำหรับสมาชิกสหภาพที่ไม่ระบุชื่อแต่ละคนที่มีสมาชิกข้อมูลที่ไม่คงที่อย่างน้อยหนึ่งสมาชิก (ถ้ามี) สมาชิกข้อมูลที่ไม่คงที่หนึ่งตัวจะมีตัวเริ่มต้นสมาชิกดีฟอลต์และ
  • แต่ละคลาสพื้นฐานที่สร้างขึ้นของ T นั้นเป็นค่าเริ่มต้นที่สร้างได้

ถ้าโปรแกรมเรียกใช้การเริ่มต้นดีฟอลต์ของอ็อบเจ็กต์ประเภท const-qualification T T จะเป็นประเภทคลาส const-default-builtible หรืออาร์เรย์ดังกล่าว

คำนี้โดยพื้นฐานแล้วหมายความว่าโค้ดที่ชัดเจนใช้งานได้ หากคุณเริ่มต้นฐานและสมาชิกทั้งหมดของคุณคุณสามารถพูดได้A const a;ไม่ว่าคุณจะสะกดตัวสร้างอย่างไรหรืออย่างไร

struct A {
};
A const a;

gcc ยอมรับสิ่งนี้ตั้งแต่ 4.6.4 clang ยอมรับสิ่งนี้ตั้งแต่ 3.9.0 Visual Studio ยังยอมรับสิ่งนี้ (อย่างน้อยในปี 2017 ไม่แน่ใจว่าเร็วกว่านั้น)


3
แต่สิ่งนี้ยังคงห้ามstruct A { int n; A() = default; }; const A a;ในขณะที่อนุญาตstruct B { int n; B() {} }; const B b;เนื่องจากถ้อยคำใหม่ยังคงระบุว่า "ผู้ใช้ระบุ" ไม่ใช่ "ประกาศโดยผู้ใช้" และฉันก็เกาหัวว่าทำไมคณะกรรมการเลือกที่จะยกเว้นตัวสร้างเริ่มต้นที่ผิดนัดอย่างชัดเจนจาก DR นี้บังคับให้เราทำ คลาสของเราไม่สำคัญหากเราต้องการวัตถุ const ที่มีสมาชิกที่ไม่ได้เริ่มต้น
Oktalist

1
น่าสนใจ แต่ยังมีกรณีขอบที่ฉันพบ ด้วยMyPODการเป็น POD struct, static MyPOD x;- อาศัยศูนย์การเริ่มต้น (เป็นที่หนึ่งใช่ไหม?) เพื่อกำหนดตัวแปรสมาชิก (s) อย่างเหมาะสม - คอมไพล์ แต่static const MyPOD x;ไม่ได้ มีโอกาสที่จะได้รับการแก้ไขหรือไม่?
Joshua Green

66

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

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

แต่ถ้าคุณทำให้ชั้นเรียนเป็นแบบไม่ใช่ POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

สังเกตความแตกต่างระหว่าง POD และ non-POD

ตัวสร้างที่ผู้ใช้กำหนดเป็นวิธีหนึ่งที่จะทำให้คลาสไม่ใช่ POD มีหลายวิธีที่คุณสามารถทำได้

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

สังเกตว่า nonPOD_B ไม่ได้กำหนดคอนสตรัคเตอร์ที่ผู้ใช้กำหนดเอง รวบรวมมัน. มันจะรวบรวม:

และแสดงความคิดเห็นเกี่ยวกับฟังก์ชันเสมือนจากนั้นแสดงข้อผิดพลาดตามที่คาดไว้:


ฉันคิดว่าคุณเข้าใจข้อความนี้ผิด ก่อนอื่นบอกว่าสิ่งนี้ (§8.5 / 9):

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

มันพูดถึงคลาสที่ไม่ใช่ POD ซึ่งอาจเป็นประเภทที่ผ่านการรับรอง CV นั่นคืออ็อบเจ็กต์ที่ไม่ใช่ POD จะถูกกำหนดค่าเริ่มต้นโดยปริยายหากไม่มีการระบุตัวเริ่มต้น และค่าเริ่มต้นเริ่มต้นคืออะไร? สำหรับ non-POD ข้อกำหนดระบุว่า (§8.5 / 5)

ในการกำหนดค่าเริ่มต้นวัตถุประเภท T หมายถึง:
- ถ้า T เป็นประเภทคลาสที่ไม่ใช่ POD (ข้อ 9) ตัวสร้างเริ่มต้นสำหรับ T จะถูกเรียก (และการกำหนดค่าเริ่มต้นจะมีรูปแบบไม่ถูกต้องหาก T ไม่มีตัวสร้างเริ่มต้นที่สามารถเข้าถึงได้)

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

หากคุณเข้าใจสิ่งนี้แล้วให้เข้าใจว่าข้อกำหนดต่อไปพูดว่าอย่างไร ((§8.5 / 9)

[... ]; ถ้าออบเจ็กต์เป็นประเภทที่มีคุณสมบัติของ const ประเภทคลาสที่อยู่ภายใต้จะต้องมีตัวสร้างเริ่มต้นที่ผู้ใช้ประกาศ

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

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

อย่างไรก็ตามสิ่งนี้คอมไพล์ได้ดีเนื่องจากไม่ใช่ POD และสามารถกำหนดค่าเริ่มต้นได้


1
ฉันเชื่อว่าตัวอย่างสุดท้ายของคุณเป็นข้อผิดพลาดในการคอมไพล์ - nonPOD_Bไม่มีตัวสร้างเริ่มต้นที่ผู้ใช้ระบุดังนั้นจึงconst nonPOD_B b2ไม่อนุญาตให้ใช้บรรทัด
Karu

1
อีกวิธีหนึ่งในการทำให้คลาสเป็น non-POD คือการกำหนดให้เป็นสมาชิกข้อมูลซึ่งไม่ใช่ POD (เช่นโครงสร้างของฉันBในคำถาม) แต่ยังคงต้องใช้ตัวสร้างเริ่มต้นที่ผู้ใช้ระบุในกรณีนั้น
Karu

"ถ้าโปรแกรมเรียกใช้การเริ่มต้นเริ่มต้นของอ็อบเจ็กต์ที่มีคุณสมบัติเป็น const ประเภท T T จะเป็นประเภทคลาสที่มีตัวสร้างเริ่มต้นที่ผู้ใช้ระบุ"
Karu

@ คารุ: เคยอ่านมาแล้ว ดูเหมือนว่ามีทางเดินอื่น ๆ ในข้อมูลจำเพาะซึ่งอนุญาตให้constอ็อบเจ็กต์ที่ไม่ใช่ POD เริ่มต้นได้โดยการเรียกใช้ default-constructor ที่สร้างโดยคอมไพเลอร์
Nawaz

2
ลิงค์ ideone ของคุณดูเหมือนจะเสียและจะดีมากถ้าคำตอบนี้สามารถอัปเดตเป็น C ++ 11/14 ได้เนื่องจาก§8.5ไม่ได้กล่าวถึง POD เลย
Oktalist

12

การคาดเดาอย่างแท้จริงในส่วนของฉัน แต่พิจารณาว่าประเภทอื่น ๆ ก็มีข้อ จำกัด ที่คล้ายกันเช่นกัน:

int main()
{
    const int i; // invalid
}

ดังนั้นกฎนี้ไม่เพียง แต่จะสอดคล้องกันเท่านั้น แต่ยังป้องกัน (เรียกซ้ำ) อ็อบเจ็กต์const(ย่อย) ที่เป็นหน่วย:

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

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

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

บางทีเราอาจกำหนดกฎเช่น 'สมาชิกอย่างน้อยหนึ่งคนจะต้องเริ่มต้นอย่างสมเหตุสมผลในตัวสร้างเริ่มต้นที่ผู้ใช้กำหนด' แต่นั่นเป็นวิธีที่ใช้เวลามากเกินไปในการพยายามป้องกัน Murphy C ++ มีแนวโน้มที่จะไว้วางใจโปรแกรมเมอร์ในบางประเด็น


แต่ด้วยการเพิ่มA(){}ข้อผิดพลาดจะหายไปดังนั้นจึงไม่ได้ป้องกันอะไร กฎไม่ทำงานซ้ำ - X(){}ไม่จำเป็นสำหรับตัวอย่างนั้น
Karu

2
อย่างน้อยก็ด้วยการบังคับให้โปรแกรมเมอร์เพิ่มตัวสร้างเขาถูกบังคับให้คิดสักนาทีกับปัญหาและอาจจะเกิดปัญหาที่ไม่สำคัญ
arne

@ คารุฉันตอบคำถามเพียงครึ่งเดียว - แก้ไขว่า :)
Luc Danton

4
@ arne: ปัญหาเดียวคือเป็นโปรแกรมเมอร์ที่ไม่ถูกต้อง ผู้ที่พยายามสร้างอินสแตนซ์ของชั้นเรียนอาจให้ความคิดทั้งหมดที่เขาต้องการในเรื่องนี้ แต่พวกเขาอาจไม่สามารถแก้ไขชั้นเรียนได้ ผู้เขียนชั้นเรียนนึกถึงสมาชิกโดยเห็นว่าพวกเขาเริ่มต้นอย่างสมเหตุสมผลโดยตัวสร้างเริ่มต้นโดยปริยายดังนั้นจึงไม่ควรเพิ่มสมาชิกเข้าไป
Karu

3
สิ่งที่ฉันนำมาจากส่วนนี้ของมาตรฐานคือ "มักจะประกาศตัวสร้างเริ่มต้นสำหรับประเภทที่ไม่ใช่ POD ในกรณีที่มีคนต้องการสร้างอินสแตนซ์ const ในวันหนึ่ง" ดูเหมือนจะมากเกินไป
Karu

3

ฉันกำลังดูการพูดคุยของ Timur Doumler ที่ Meeting C ++ 2018 และในที่สุดฉันก็รู้ว่าทำไมมาตรฐานจึงต้องใช้ตัวสร้างที่ผู้ใช้จัดหาที่นี่ไม่ใช่แค่ผู้ใช้ประกาศเท่านั้น มันเกี่ยวข้องกับกฎสำหรับการเริ่มต้นค่า

พิจารณาสองคลาส: Aมีคอนสตรัคเตอร์ที่ผู้ใช้ประกาศมีคอนสตรัคเตอร์Bที่ผู้ใช้ระบุ :

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

เมื่อมองแวบแรกคุณอาจคิดว่าตัวสร้างทั้งสองนี้จะทำงานเหมือนกัน แต่ดูว่าการกำหนดค่าเริ่มต้นทำงานแตกต่างกันอย่างไรในขณะที่การเริ่มต้นเริ่มต้นเท่านั้นทำงานเหมือนกัน:

  • A a;เป็นค่าเริ่มต้นเริ่มต้น: สมาชิกint xไม่ได้เริ่มต้น
  • B b;เป็นค่าเริ่มต้นเริ่มต้น: สมาชิกint xไม่ได้เริ่มต้น
  • A a{};คุ้มค่า initialisation: สมาชิกint xเป็นศูนย์ initialised
  • B b{};คือการเริ่มต้นค่า: สมาชิกint xไม่ได้เริ่มต้น

ตอนนี้ดูว่าจะเกิดอะไรขึ้นเมื่อเราเพิ่มconst:

  • const A a;เป็นค่าเริ่มต้นเริ่มต้น: นี่เป็นรูปแบบที่ไม่ถูกต้องเนื่องจากกฎที่อ้างในคำถาม
  • const B b;เป็นค่าเริ่มต้นเริ่มต้น: สมาชิกint xไม่ได้เริ่มต้น
  • const A a{}; คือค่าเริ่มต้น: สมาชิก int xเป็นศูนย์ initialised
  • const B b{};คือการเริ่มต้นค่า: สมาชิกint xไม่ได้เริ่มต้น

constสเกลาร์ที่ไม่ได้กำหนดค่าเริ่มต้น (เช่นint xสมาชิก) จะไร้ประโยชน์: การเขียนถึงมันไม่ถูกต้อง (เพราะมันconst) และการอ่านจากมันคือ UB (เนื่องจากมีค่าที่ไม่แน่นอน) ดังนั้นกฎนี้จึงป้องกันไม่ให้คุณสร้างสิ่งนั้นขึ้นมาโดยบังคับให้คุณทำอย่างใดอย่างหนึ่งเพิ่ม initialiser หรือเลือกในกับพฤติกรรมที่เป็นอันตรายโดยการเพิ่มคอนสตรัคที่ผู้ใช้ให้

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


1

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

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

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

คำถามคือมีเหตุผลที่น่าสนใจหรือไม่ที่กฎควรจะซับซ้อนกว่านี้ มีโค้ดบางอย่างที่ยากต่อการเขียนหรือทำความเข้าใจที่สามารถเขียนได้ง่ายกว่ามากถ้ากฎซับซ้อนกว่านี้?


1
นี่คือคำที่แนะนำของฉัน: "ถ้าโปรแกรมเรียกร้องให้เริ่มต้นอ็อบเจ็กต์ที่มีคุณสมบัติเป็นประเภท const T T จะเป็นประเภทคลาสที่ไม่ใช่ POD" สิ่งนี้จะทำให้const POD x;ผิดกฎหมายเช่นเดียวกับconst int x;ผิดกฎหมาย (ซึ่งสมเหตุสมผลเพราะสิ่งนี้ไม่มีประโยชน์สำหรับ POD) แต่ทำให้const NonPOD x;ถูกกฎหมาย (ซึ่งสมเหตุสมผลเพราะอาจมีวัตถุย่อยที่มีตัวสร้าง / ตัวทำลายที่เป็นประโยชน์หรือมีตัวสร้าง / ตัวทำลายที่มีประโยชน์เอง) .
Karu

@ คารุ - คำพูดนั้นอาจใช้ได้ผล ฉันคุ้นเคยกับมาตรฐาน RFC และรู้สึกว่า 'T จะเป็น' ควรอ่าน 'T must be' แต่ใช่ว่าจะได้ผล
Omnifarious

@ คารุ - แล้ว Struct NonPod {int i; โมฆะเสมือน f () {}}? มันไม่สมเหตุสมผลที่จะสร้าง const NonPod x; ถูกกฎหมาย.
gruzovator

1
@gruzovator มันจะสมเหตุสมผลกว่านี้ไหมถ้าคุณมีตัวสร้างเริ่มต้นที่ผู้ใช้ประกาศว่างเปล่า? ข้อเสนอแนะของฉันพยายามที่จะลบข้อกำหนดที่ไม่มีจุดหมายของมาตรฐาน มีหรือไม่มีก็ยังมีอีกหลายวิธีในการเขียนโค้ดที่ไม่สมเหตุสมผล
Karu

1
@ คารุฉันเห็นด้วยกับคุณ เนื่องจากกฎนี้ในมาตรฐานมีหลายคลาสที่ต้องมีตัวสร้างว่างที่ผู้ใช้กำหนดเอง ฉันชอบพฤติกรรม gcc อนุญาตเช่นstruct NonPod { std::string s; }; const NonPod x;และให้ข้อผิดพลาดเมื่อ NonPod คือstruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.