“ = ค่าเริ่มต้น” แตกต่างจาก“ {}” สำหรับตัวสร้างและตัวทำลายเริ่มต้นอย่างไร


169

ฉันโพสต์สิ่งนี้เป็นคำถามเกี่ยวกับ destructors เท่านั้น แต่ตอนนี้ฉันเพิ่มการพิจารณาตัวสร้างเริ่มต้น นี่คือคำถามเดิม:

ถ้าฉันต้องการให้ destructor ในชั้นเรียนของฉันเป็นเสมือน แต่อย่างอื่นเหมือนกับสิ่งที่คอมไพเลอร์จะสร้างฉันสามารถใช้=default:

class Widget {
public:
   virtual ~Widget() = default;
};

แต่ดูเหมือนว่าฉันจะได้รับผลกระทบเดียวกันโดยพิมพ์น้อยลงโดยใช้คำจำกัดความที่ว่างเปล่า:

class Widget {
public:
   virtual ~Widget() {}
};

มีวิธีใดบ้างที่คำจำกัดความทั้งสองนี้ทำงานต่างกัน?

จากการตอบกลับที่โพสต์สำหรับคำถามนี้สถานการณ์ของ Constructor เริ่มต้นจะคล้ายกัน ระบุว่าไม่มีความแตกต่างในความหมายระหว่าง " =default" และ " {}" สำหรับ destructors มีความคล้ายคลึงกันเกือบไม่แตกต่างกันในความหมายระหว่างตัวเลือกเหล่านี้สำหรับตัวสร้างเริ่มต้นหรือไม่ นั่นคือสมมติว่าฉันต้องการสร้างประเภทที่วัตถุประเภทนั้นจะถูกสร้างขึ้นและถูกทำลายทำไมฉันอยากจะพูด

Widget() = default;

แทน

Widget() {}

?

ฉันขออภัยหากการขยายคำถามนี้หลังจากการโพสต์ต้นฉบับละเมิดกฎ SO ดังนั้น การโพสต์คำถามที่เกือบเหมือนกันสำหรับตัวสร้างเริ่มต้นทำให้ฉันเป็นตัวเลือกที่ต้องการน้อยกว่า


1
ไม่ใช่ที่ฉันรู้ แต่= defaultเป็น imo ที่ชัดเจนกว่าและสอดคล้องกับการสนับสนุนสำหรับ constructor
chris

11
ฉันไม่รู้แน่ชัด แต่ฉันคิดว่าอดีตสอดคล้องกับคำจำกัดความของ "destructor เล็กน้อย" ในขณะที่หลังไม่ได้ ดังนั้นstd::has_trivial_destructor<Widget>::valueเป็นtrueครั้งแรก แต่falseเป็นครั้งที่สอง ความหมายของสิ่งนั้นคืออะไรฉันไม่รู้เช่นกัน :)
GManNickG

10
destructor เสมือนนั้นไม่สำคัญเลย
Luc Danton

@LucDanton: ฉันคิดว่าเปิดตาของฉันและดูรหัสจะทำงานด้วย! ขอบคุณสำหรับการแก้ไข
GManNickG

ที่เกี่ยวข้อง: stackoverflow.com/questions/20828907/…
Gabriel Staples

คำตอบ:


103

นี่เป็นคำถามที่แตกต่างอย่างสิ้นเชิงเมื่อถามเกี่ยวกับการก่อสร้างกว่า destructors

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

การใช้= defaultไวยากรณ์สำหรับฟังก์ชั่นสมาชิกพิเศษ (คอนสตรัคเตอร์เริ่มต้น, คัดลอก / ย้ายคอนสตรัคเตอร์ / การมอบหมาย, destructors ฯลฯ ) หมายถึงสิ่งที่แตกต่างจากการทำ{}หมายถึงสิ่งที่แตกต่างกันมากจากเพียงแค่การทำ ด้วยฟังก์ชันหลังกลายเป็น "ผู้ใช้ให้" และนั่นเป็นการเปลี่ยนแปลงทุกอย่าง

นี่เป็นคลาสที่ไม่สำคัญตามคำนิยามของ C ++ 11:

struct Trivial
{
  int foo;
};

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

นี้:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

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

นี้:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

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

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


2
ดังนั้นประเด็นสำคัญน่าจะเป็นไม่ว่าจะเป็นระดับที่เกิดขึ้นเป็นที่น่ารำคาญและพื้นฐานปัญหาที่เป็นความแตกต่างระหว่างฟังก์ชั่นพิเศษที่ถูกใช้ประกาศ (ซึ่งเป็นกรณีสำหรับ=defaultฟังก์ชั่น) และผู้ใช้ให้ไว้ (ซึ่งเป็นกรณีสำหรับ{}ฟังก์ชั่น) ทั้งฟังก์ชั่นที่ผู้ใช้ประกาศและผู้ใช้ให้สามารถป้องกันการสร้างฟังก์ชั่นสมาชิกพิเศษอื่น ๆ (เช่นผู้ทำลายที่ประกาศโดยผู้ใช้ป้องกันการสร้างการดำเนินการย้าย) แต่ฟังก์ชั่นพิเศษที่ผู้ใช้ให้ไว้เท่านั้น ขวา?
KnowItAllWannabe

@KnowItAllWannabe: นั่นเป็นความคิดทั่วไปใช่
Nicol Bolas

ฉันเลือกสิ่งนี้เป็นคำตอบที่ยอมรับได้เพียงเพราะมันครอบคลุมทั้งตัวสร้างและ (โดยอ้างอิงจากคำตอบของฮาวเวิร์ด)
KnowItAllWannabe

ดูเหมือนจะเป็นคำที่หายไปในที่นี้ "เท่าที่กฎของ C ++ 11 มีความกังวลคุณมีสิทธิ์ของประเภทเล็กน้อย" ฉันจะแก้ไข แต่ฉันไม่แน่ใจ 100% ว่าสิ่งที่ตั้งใจ
jcoder

2
= defaultดูเหมือนว่าจะมีประโยชน์สำหรับการบังคับให้คอมไพเลอร์เพื่อสร้างคอนสตรัคเตอร์เริ่มต้นแม้จะมีการก่อสร้างอื่น ๆ ; ตัวสร้างเริ่มต้นจะไม่ถูกประกาศโดยปริยายหากมีการจัดทำโครงสร้างที่ผู้ใช้ประกาศอื่น
bgfvdu3w

42

พวกเขาทั้งสองไม่สำคัญ

พวกเขาทั้งสองมีข้อกำหนด noexcept เดียวกันขึ้นอยู่กับข้อกำหนด noexcept ของฐานและสมาชิก

ความแตกต่างเพียงอย่างเดียวที่ฉันตรวจพบก็คือถ้าWidgetมีฐานหรือสมาชิกที่มี destructor ที่ไม่สามารถเข้าถึงหรือลบได้:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

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

Otoh ถ้าคุณให้ destructor ที่ผู้ใช้จัดหาให้สิ่งต่าง ๆ จะไม่รวบรวมว่าคุณจะทำการทำลายหรือไม่Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.

9
สิ่งที่น่าสนใจ: กล่าวอีกนัยหนึ่งว่า=default;คอมไพเลอร์จะไม่สร้าง destructor ยกเว้นว่ามันถูกใช้และจะไม่เกิดข้อผิดพลาด สิ่งนี้ดูแปลกสำหรับฉันแม้ว่าจะไม่ใช่ข้อผิดพลาดก็ตาม ฉันนึกภาพไม่ออกว่าพฤติกรรมนี้ได้รับคำสั่งในมาตรฐาน
Nik Bougalis

"จากนั้น = โซลูชันเริ่มต้นจะรวบรวม" ไม่มันจะไม่ เพิ่งทดสอบใน vs.
nano

ข้อความแสดงข้อผิดพลาดคืออะไรและ VS รุ่นใด
Howard Hinnant

35

ความแตกต่างที่สำคัญระหว่าง

class B {
    public:
    B(){}
    int i;
    int j;
};

และ

class B {
    public:
    B() = default;
    int i;
    int j;
};

เป็นตัวสร้างเริ่มต้นที่กำหนดด้วย B() = default;ถือว่าไม่ที่ผู้ใช้กำหนด ซึ่งหมายความว่าในกรณีของการกำหนดค่าเริ่มต้นเช่นเดียวกับใน

B* pb = new B();  // use of () triggers value-initialization

ชนิดพิเศษของการเริ่มต้นที่ไม่ได้ใช้คอนสตรัคที่ทุกคนจะใช้สถานที่และในตัวชนิดนี้จะส่งผลในการเป็นศูนย์การเริ่มต้น ในกรณีB(){}นี้จะไม่เกิดขึ้น มาตรฐาน C ++ n3337 § 8.5 / 7 กล่าวว่า

ในการกำหนดค่าเริ่มต้นวัตถุประเภท T หมายถึง:

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

- ถ้า T เป็นประเภทคลาสที่ไม่ใช่สหภาพ (อาจเป็น cv) โดยไม่ต้องมีคอนสตรัคเตอร์ที่ผู้ใช้จัดเตรียมไว้ดังนั้นวัตถุจะถูกกำหนดค่าเริ่มต้นเป็นศูนย์และหาก T ของคอนสตรัคเตอร์ปริยายที่ประกาศโดยปริยาย

- ถ้า T เป็นประเภทอาเรย์แต่ละองค์ประกอบจะถูกกำหนดค่าเริ่มต้น - มิฉะนั้นวัตถุจะไม่มีค่าเริ่มต้น

ตัวอย่างเช่น:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

ผลลัพธ์ที่เป็นไปได้:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd


แล้วทำไม "{}" และ "= เริ่มต้น" มักจะเริ่มต้นมาตรฐาน :: สตริงideone.com/LMv5Uf ?
nawfel bgh

1
@nawfelbgh ตัวสร้างเริ่มต้น A () {} เรียก constructor เริ่มต้นสำหรับ std :: string เนื่องจากนี่ไม่ใช่ชนิด POD ctor เริ่มต้นของ std :: string เริ่มต้นให้เป็นสตริงว่างขนาด 0 ctor เริ่มต้นสำหรับสเกลาร์ไม่ได้ทำอะไรเลย: ออบเจ็กต์ที่มีระยะเวลาการจัดเก็บอัตโนมัติ
4pie0
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.