ไวยากรณ์ใหม่“ = default” ใน C ++ 11


144

ฉันไม่เข้าใจว่าทำไมฉันถึงทำสิ่งนี้:

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

ทำไมไม่พูดว่า:

S() {} // instead of S() = default;

ทำไมต้องนำไวยากรณ์ใหม่มาใช้


31
Nitpick: defaultไม่ใช่คีย์เวิร์ดใหม่เป็นเพียงการใช้คีย์เวิร์ดใหม่ที่จองไว้แล้ว


Mey be คำถามนี้อาจช่วยคุณได้
FreeNickname

8
นอกจากคำตอบอื่น ๆ แล้วฉันยังยืนยันด้วยว่า '= default;' เป็นการจัดทำเอกสารด้วยตนเองมากขึ้น
ทำเครื่องหมาย

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

คำตอบ:


139

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

§12.1 / 6 [class.ctor] คอนสตรัคเตอร์ดีฟอลต์ที่เป็นค่าเริ่มต้นและไม่ได้กำหนดให้ถูกลบถูกกำหนดโดยปริยายเมื่อใช้ odr เพื่อสร้างอ็อบเจ็กต์ประเภทคลาสหรือเมื่อมีการดีฟอลต์อย่างชัดเจนหลังจากการประกาศครั้งแรก ตัวสร้างเริ่มต้นที่กำหนดโดยนัยจะดำเนินการชุดของการเริ่มต้นของคลาสที่จะดำเนินการโดยตัวสร้างเริ่มต้นที่ผู้ใช้เขียนขึ้นสำหรับคลาสนั้นโดยไม่มี ctor-initializer (12.6.2) และคำสั่งผสมที่ว่างเปล่า [... ]

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

§8.5.1 / 1 [dcl.init.aggr]การรวมคืออาร์เรย์หรือคลาสที่ไม่มีตัวสร้างที่ผู้ใช้ระบุ [และ ... ]

§12.1 / 5 [class.ctor] คอนสตรัคเตอร์เริ่มต้นจะไม่สำคัญหากไม่ได้ให้โดยผู้ใช้และ [... ]

§9 / 6 [คลาส] คลาสเล็กน้อยคือคลาสที่มีคอนสตรัคเตอร์เริ่มต้นเล็กน้อยและ [... ]

เพื่อแสดงให้เห็น:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");
    
    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

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

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


เกือบ. 12.1 / 6: "หากตัวสร้างเริ่มต้นที่ผู้ใช้เขียนขึ้นนั้นจะเป็นไปตามข้อกำหนดของตัวconstexprสร้าง (7.1.5) ตัวสร้างเริ่มต้นที่กำหนดโดยนัยคือconstexpr"
Casey

ที่จริงแล้ว 8.4.2 / 2 ให้ข้อมูลมากกว่า: "หากฟังก์ชันผิดนัดอย่างชัดเจนในการประกาศครั้งแรก (ก) จะถือว่าโดยปริยายconstexprว่าเป็นการประกาศโดยปริยายหรือไม่ (b) ถือว่าโดยปริยายมีความเหมือนกัน ยกเว้นข้อกำหนดราวกับว่ามันได้รับการประกาศโดยปริยาย (15.4) ..." มันทำให้ความแตกต่างในกรณีเฉพาะนี้ แต่โดยทั่วไปมีข้อได้เปรียบกว่าเล็กน้อยfoo() = default; foo() {}
Casey

2
คุณบอกว่าไม่มีความแตกต่างแล้วอธิบายความแตกต่างต่อไป?

@hvd ในกรณีนี้ไม่มีความแตกต่างเนื่องจากการประกาศโดยปริยายจะไม่เป็นconstexpr(เนื่องจากสมาชิกข้อมูลถูกปล่อยทิ้งไว้โดยไม่ได้เริ่มต้น) และข้อกำหนดข้อยกเว้นอนุญาตให้มีข้อยกเว้นทั้งหมด ฉันจะทำให้ชัดเจนขึ้น
Joseph Mansfield

2
ขอขอบคุณสำหรับการชี้แจง. มีไม่ยังดูเหมือนจะมีความแตกต่างกัน แต่ด้วยconstexpr(ซึ่งคุณกล่าวถึงไม่ควรสร้างความแตกต่างที่นี่): struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};เพียงให้ข้อผิดพลาดไม่ได้s1 s2ทั้งในเสียงดังและ g ++

11

ฉันมีตัวอย่างที่จะแสดงความแตกต่าง:

#include <iostream>

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

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


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

เอาท์พุต:

5
5
0

ดังที่เราเห็นการเรียกตัวสร้าง A () ว่างเปล่าไม่ได้เริ่มต้นสมาชิกในขณะที่ B () ทำ


8
คุณช่วยอธิบายไวยากรณ์นี้ได้ไหม -> new (& x) A ();
Vencat

6
เรากำลังสร้างวัตถุใหม่ในหน่วยความจำโดยเริ่มจากที่อยู่ของตัวแปร x (แทนการจัดสรรหน่วยความจำใหม่) ไวยากรณ์นี้ใช้เพื่อสร้างวัตถุในหน่วยความจำที่จัดสรรไว้ล่วงหน้า เช่นเดียวกับในกรณีของเราขนาด B = ขนาดของ int ดังนั้นใหม่ (& x) A () จะสร้างวัตถุใหม่ในตำแหน่งของตัวแปร x
Slavenskij

1
ฉันได้ผลลัพธ์ที่แตกต่างกับ gcc 8.3: ideone.com/XouXux
Adam.Er8

แม้จะใช้ C ++ 14 แต่ฉันก็ได้ผลลัพธ์ที่แตกต่างกัน: ideone.com/CQphuT
Mayank Bhushan

9

n2210มีเหตุผลบางประการ:

การจัดการค่าเริ่มต้นมีปัญหาหลายประการ:

  • คำจำกัดความของตัวสร้างเป็นคู่กัน การประกาศตัวสร้างใด ๆ จะยับยั้งตัวสร้างเริ่มต้น
  • ค่าดีฟอลต์ destructor ไม่เหมาะสมกับคลาส polymorphic ซึ่งต้องการคำจำกัดความที่ชัดเจน
  • เมื่อระงับค่าเริ่มต้นแล้วจะไม่มีวิธีการคืนชีพ
  • การใช้งานตามค่าเริ่มต้นมักจะมีประสิทธิภาพมากกว่าการใช้งานที่ระบุด้วยตนเอง
  • การใช้งานที่ไม่ใช่ค่าเริ่มต้นนั้นไม่สำคัญซึ่งส่งผลต่อความหมายของประเภทเช่นทำให้ประเภทที่ไม่ใช่ POD
  • ไม่มีวิธีการห้ามการทำงานของสมาชิกพิเศษหรือตัวดำเนินการส่วนกลางโดยไม่ต้องประกาศตัวทดแทน (ที่ไม่สำคัญ)

type::type() = default;
type::type() { x = 3; }

ในบางกรณีส่วนของคลาสสามารถเปลี่ยนแปลงได้โดยไม่ต้องมีการเปลี่ยนแปลงนิยามฟังก์ชันสมาชิกเนื่องจากค่าดีฟอลต์จะเปลี่ยนไปพร้อมกับการประกาศสมาชิกเพิ่มเติม

ดูRule-of-Three กลายเป็น Rule-of-Five ด้วย C ++ 11? :

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


1
พวกเขามีเหตุผลสำหรับการมี= defaultโดยทั่วไปมากกว่าเหตุผลสำหรับการทำในคอนสตรัคเทียบกับการทำ= default { }
Joseph Mansfield

@JosephMansfield ทรู แต่เนื่องจาก{}มีอยู่แล้วคุณลักษณะของภาษาก่อนที่จะนำมา=defaultด้วยเหตุผลเหล่านี้ไม่ปริยายพึ่งพาความแตกต่าง (เช่น "มีวิธีการที่จะรื้อฟื้น [เริ่มต้นปราบปราม] ไม่มี" หมายความว่า{}เป็นไม่ได้เทียบเท่ากับการเริ่มต้น ).
Kyle Strand

7

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

สำหรับการสร้างเริ่มต้นก็จะได้รับเป็นไปได้ที่จะทำให้การสร้างเริ่มต้นใด ๆ =defaultกับร่างกายที่ว่างเปล่าได้รับการพิจารณาผู้สมัครที่เป็นตัวสร้างเล็กน้อยเช่นเดียวกับการใช้ หลังจากที่ทุกคนเก่าก่อสร้างเริ่มต้นที่ว่างเปล่าอยู่ตามกฎหมาย c ++

struct S { 
  int a; 
  S() {} // legal C++ 
};

ไม่ว่าคอมไพเลอร์จะเข้าใจตัวสร้างนี้เป็นเรื่องเล็กน้อยหรือไม่นั้นไม่เกี่ยวข้องในกรณีส่วนใหญ่นอกเหนือจากการปรับให้เหมาะสม (ด้วยตนเองหรือคอมไพเลอร์)

อย่างไรก็ตามความพยายามนี้จะถือว่าเนื้อหาของฟังก์ชันว่างเป็น "ค่าเริ่มต้น" จะแบ่งออกทั้งหมดสำหรับฟังก์ชันสมาชิกประเภทอื่น ๆ พิจารณาตัวสร้างการคัดลอก:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

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

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};

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

จากนั้นลองพิจารณาตัวดำเนินการกำหนดสำเนาซึ่งอาจทำให้ขนขึ้นได้โดยเฉพาะอย่างยิ่งในกรณีที่ไม่สำคัญ เป็นหม้อไอน้ำจำนวนมากที่คุณไม่ต้องการเขียนสำหรับหลาย ๆ ชั้นเรียน แต่คุณจะถูกบังคับให้อยู่ใน C ++ 03:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

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

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

อาจเป็นการดีที่จะสร้างตัวสร้างเริ่มต้นที่มีเนื้อความว่างเปล่าและตัวสร้างสมาชิก / ฐานที่ไม่สำคัญก็ถือว่าเป็นเรื่องเล็กน้อยเช่นเดียวกับที่เคยเป็นมา=defaultหากเพียงเพื่อทำให้โค้ดที่เก่ากว่าเหมาะสมยิ่งขึ้นในบางกรณี แต่โค้ดระดับต่ำส่วนใหญ่จะอาศัยความไม่สำคัญ ตัวสร้างเริ่มต้นสำหรับการเพิ่มประสิทธิภาพยังอาศัยตัวสร้างการคัดลอกเล็กน้อย หากคุณจะต้องไปและ "แก้ไข" ตัวสร้างสำเนาเก่าทั้งหมดของคุณการแก้ไขตัวสร้างเริ่มต้นเดิมทั้งหมดของคุณก็ไม่ได้เป็นเรื่องที่ต้องทำ นอกจากนี้ยังชัดเจนและชัดเจนยิ่งขึ้นโดยใช้ Explicit =defaultเพื่อแสดงถึงความตั้งใจของคุณ

มีสิ่งอื่น ๆ อีกสองสามอย่างที่ฟังก์ชันสมาชิกที่สร้างโดยคอมไพเลอร์จะทำซึ่งคุณจะต้องทำการเปลี่ยนแปลงเพื่อรองรับอย่างชัดเจนเช่นกัน การสนับสนุนตัวconstexprสร้างเริ่มต้นเป็นตัวอย่างหนึ่ง มันง่ายกว่าที่จะใช้ในทางจิตใจ=defaultมากกว่าการมาร์กอัปฟังก์ชันด้วยคีย์เวิร์ดพิเศษอื่น ๆ ทั้งหมดและโดยนัย=defaultและนั่นเป็นหนึ่งในธีมของ C ++ 11: ทำให้ภาษาง่ายขึ้น มันยังคงมีหูดและการประนีประนอมกลับเข้ากันได้มากมาย แต่เป็นที่ชัดเจนว่านี่เป็นก้าวที่ยิ่งใหญ่จาก C ++ 03 ในเรื่องความสะดวกในการใช้งาน


ฉันมีปัญหาที่ฉันคาดว่า= defaultจะทำa=0;และไม่ได้! : a(0)ฉันได้ไปวางไว้ในความโปรดปรานของ ตอนนี้ยังงง ๆ อยู่ว่ามีประโยชน์แค่ไหน= defaultมันเกี่ยวกับประสิทธิภาพหรือเปล่า? มันจะพังไหมถ้าไม่ใช้= default? ฉันลองอ่านคำตอบทั้งหมดที่นี่ซื้อฉันยังใหม่กับบางสิ่ง c ++ และฉันมีปัญหาในการทำความเข้าใจมันมาก
Aquarius Power

@AquariusPower: ไม่ใช่ "แค่" เกี่ยวกับประสิทธิภาพ แต่ยังจำเป็นในบางกรณีเกี่ยวกับข้อยกเว้นและความหมายอื่น ๆ กล่าวคือตัวดำเนินการเริ่มต้นอาจเป็นเรื่องเล็กน้อย แต่ตัวดำเนินการที่ไม่ใช่ค่าเริ่มต้นไม่สามารถเป็นเรื่องเล็กน้อยได้และบางรหัสจะใช้เทคนิคการเขียนโปรแกรมเมตาเพื่อปรับเปลี่ยนพฤติกรรมสำหรับหรือแม้แต่ไม่อนุญาตประเภทด้วยการดำเนินการที่ไม่สำคัญ a=0ตัวอย่างของคุณเป็นเพราะพฤติกรรมของประเภทเล็กน้อยซึ่งเป็นหัวข้อแยกต่างหาก (แม้ว่าจะเกี่ยวข้องกัน)
Sean Middleditch

หมายความว่าเป็นไปได้หรือไม่ที่จะมี= defaultและยังคงให้aอยู่=0? ไม่ทางใดก็ทางหนึ่ง? คุณคิดว่าฉันสามารถสร้างคำถามใหม่เช่น "การมีตัวสร้าง= defaultและการให้สิทธิ์ฟิลด์จะได้รับการเริ่มต้นอย่างถูกต้องหรือไม่" btw ฉันมีปัญหาใน a structไม่ใช่ a classและแอปทำงานอย่างถูกต้องแม้ว่าจะไม่ได้ใช้= defaultงานฉันก็ทำได้ เพิ่มโครงสร้างที่น้อยที่สุดสำหรับคำถามนั้นหากเป็นคำถามที่ดี :)
Aquarius Power

1
@AquariusPower: คุณสามารถใช้ initializers ข้อมูลสมาชิกแบบไม่คงที่ เขียนโครงสร้างของคุณดังนี้: struct { int a = 0; };หากคุณตัดสินใจว่าต้องการตัวสร้างคุณสามารถเริ่มต้นได้ แต่โปรดทราบว่าประเภทจะไม่สำคัญ (ซึ่งก็ใช้ได้)
Sean Middleditch

2

เนื่องจากการเลิกใช้งานstd::is_podและทางเลือกอื่นstd::is_trivial && std::is_standard_layoutตัวอย่างข้อมูลจาก @JosephMansfield กลายเป็น:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

โปรดทราบว่าYยังคงเป็นรูปแบบมาตรฐาน

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