ฉันไม่เข้าใจว่าทำไมฉันถึงทำสิ่งนี้:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
ทำไมไม่พูดว่า:
S() {} // instead of S() = default;
ทำไมต้องนำไวยากรณ์ใหม่มาใช้
ฉันไม่เข้าใจว่าทำไมฉันถึงทำสิ่งนี้:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
ทำไมไม่พูดว่า:
S() {} // instead of S() = default;
ทำไมต้องนำไวยากรณ์ใหม่มาใช้
คำตอบ:
ตัวสร้างเริ่มต้นที่เป็นค่าเริ่มต้นถูกกำหนดโดยเฉพาะให้เหมือนกับตัวสร้างเริ่มต้นที่ผู้ใช้กำหนดโดยไม่มีรายการเริ่มต้นและคำสั่งผสมว่าง
§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
) อย่างสม่ำเสมอสำหรับแต่ละฟังก์ชันสมาชิกพิเศษเหล่านี้ทำให้โค้ดของคุณอ่านง่ายขึ้นโดยระบุเจตนาของคุณอย่างชัดเจน
constexpr
สร้าง (7.1.5) ตัวสร้างเริ่มต้นที่กำหนดโดยนัยคือconstexpr
"
constexpr
ว่าเป็นการประกาศโดยปริยายหรือไม่ (b) ถือว่าโดยปริยายมีความเหมือนกัน ยกเว้นข้อกำหนดราวกับว่ามันได้รับการประกาศโดยปริยาย (15.4) ..." มันทำให้ความแตกต่างในกรณีเฉพาะนี้ แต่โดยทั่วไปมีข้อได้เปรียบกว่าเล็กน้อยfoo() = default;
foo() {}
constexpr
(เนื่องจากสมาชิกข้อมูลถูกปล่อยทิ้งไว้โดยไม่ได้เริ่มต้น) และข้อกำหนดข้อยกเว้นอนุญาตให้มีข้อยกเว้นทั้งหมด ฉันจะทำให้ชัดเจนขึ้น
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 ++
ฉันมีตัวอย่างที่จะแสดงความแตกต่าง:
#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 () ทำ
n2210มีเหตุผลบางประการ:
การจัดการค่าเริ่มต้นมีปัญหาหลายประการ:
- คำจำกัดความของตัวสร้างเป็นคู่กัน การประกาศตัวสร้างใด ๆ จะยับยั้งตัวสร้างเริ่มต้น
- ค่าดีฟอลต์ destructor ไม่เหมาะสมกับคลาส polymorphic ซึ่งต้องการคำจำกัดความที่ชัดเจน
- เมื่อระงับค่าเริ่มต้นแล้วจะไม่มีวิธีการคืนชีพ
- การใช้งานตามค่าเริ่มต้นมักจะมีประสิทธิภาพมากกว่าการใช้งานที่ระบุด้วยตนเอง
- การใช้งานที่ไม่ใช่ค่าเริ่มต้นนั้นไม่สำคัญซึ่งส่งผลต่อความหมายของประเภทเช่นทำให้ประเภทที่ไม่ใช่ POD
- ไม่มีวิธีการห้ามการทำงานของสมาชิกพิเศษหรือตัวดำเนินการส่วนกลางโดยไม่ต้องประกาศตัวทดแทน (ที่ไม่สำคัญ)
type::type() = default; type::type() { x = 3; }
ในบางกรณีส่วนของคลาสสามารถเปลี่ยนแปลงได้โดยไม่ต้องมีการเปลี่ยนแปลงนิยามฟังก์ชันสมาชิกเนื่องจากค่าดีฟอลต์จะเปลี่ยนไปพร้อมกับการประกาศสมาชิกเพิ่มเติม
ดูRule-of-Three กลายเป็น Rule-of-Five ด้วย C ++ 11? :
โปรดทราบว่าตัวสร้างการย้ายและตัวดำเนินการกำหนดย้ายจะไม่ถูกสร้างขึ้นสำหรับคลาสที่ประกาศฟังก์ชันสมาชิกพิเศษอื่น ๆ อย่างชัดเจนตัวสร้างการคัดลอกและตัวดำเนินการกำหนดสำเนาจะไม่ถูกสร้างขึ้นสำหรับคลาสที่ประกาศตัวสร้างการย้ายหรือการย้ายอย่างชัดเจน ตัวดำเนินการกำหนดและคลาสที่มีตัวสร้างการทำลายล้างที่ประกาศไว้อย่างชัดเจนและตัวสร้างการคัดลอกที่กำหนดโดยนัยหรือตัวดำเนินการกำหนดสำเนาที่กำหนดไว้โดยปริยายถือว่าเลิกใช้แล้ว
= default
โดยทั่วไปมากกว่าเหตุผลสำหรับการทำในคอนสตรัคเทียบกับการทำ= default
{ }
{}
มีอยู่แล้วคุณลักษณะของภาษาก่อนที่จะนำมา=default
ด้วยเหตุผลเหล่านี้ไม่ปริยายพึ่งพาความแตกต่าง (เช่น "มีวิธีการที่จะรื้อฟื้น [เริ่มต้นปราบปราม] ไม่มี" หมายความว่า{}
เป็นไม่ได้เทียบเท่ากับการเริ่มต้น ).
มันเป็นเรื่องของความหมายในบางกรณี ไม่ชัดเจนมากนักกับตัวสร้างเริ่มต้น แต่จะเห็นได้ชัดกับฟังก์ชันสมาชิกที่สร้างโดยคอมไพเลอร์อื่น ๆ
สำหรับการสร้างเริ่มต้นก็จะได้รับเป็นไปได้ที่จะทำให้การสร้างเริ่มต้นใด ๆ =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 ++ และฉันมีปัญหาในการทำความเข้าใจมันมาก
a=0
ตัวอย่างของคุณเป็นเพราะพฤติกรรมของประเภทเล็กน้อยซึ่งเป็นหัวข้อแยกต่างหาก (แม้ว่าจะเกี่ยวข้องกัน)
= default
และยังคงให้a
อยู่=0
? ไม่ทางใดก็ทางหนึ่ง? คุณคิดว่าฉันสามารถสร้างคำถามใหม่เช่น "การมีตัวสร้าง= default
และการให้สิทธิ์ฟิลด์จะได้รับการเริ่มต้นอย่างถูกต้องหรือไม่" btw ฉันมีปัญหาใน a struct
ไม่ใช่ a class
และแอปทำงานอย่างถูกต้องแม้ว่าจะไม่ได้ใช้= default
งานฉันก็ทำได้ เพิ่มโครงสร้างที่น้อยที่สุดสำหรับคำถามนั้นหากเป็นคำถามที่ดี :)
struct { int a = 0; };
หากคุณตัดสินใจว่าต้องการตัวสร้างคุณสามารถเริ่มต้นได้ แต่โปรดทราบว่าประเภทจะไม่สำคัญ (ซึ่งก็ใช้ได้)
เนื่องจากการเลิกใช้งาน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
ยังคงเป็นรูปแบบมาตรฐาน
default
ไม่ใช่คีย์เวิร์ดใหม่เป็นเพียงการใช้คีย์เวิร์ดใหม่ที่จองไว้แล้ว