ทำไมการเริ่มต้นรายการ (โดยใช้เครื่องหมายปีกกาแบบโค้ง) ดีกว่าตัวเลือกอื่น ๆ


405
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

ทำไม?

ฉันไม่พบคำตอบใน SO ดังนั้นให้ฉันตอบคำถามของฉันเอง


12
ทำไมไม่ใช้auto?
มาร์คการ์เซีย

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

102
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratorต้องการคำกับคุณ
Xeo

9
@Oleksiy ฉันแนะนำให้อ่านGotWนี้
Rapptz

17
@doc ผมว่าusing MyContainer = std::map<std::string, std::vector<std::string>>;จะดียิ่งขึ้น (โดยเฉพาะอย่างยิ่งคุณสามารถแม่มัน!)
JAB

คำตอบ:


356

โดยทั่วไปการคัดลอกและวางจาก Bjarne Stroustrup ของ"The C ++ Programming Language 4th Edition" :

รายการเริ่มต้นไม่อนุญาตให้แคบลง (§iso.8.5.4) นั่นคือ:

  • จำนวนเต็มไม่สามารถแปลงเป็นจำนวนเต็มที่ไม่สามารถเก็บค่าได้ ตัวอย่างเช่นอนุญาตให้ใช้ char ถึง int แต่ไม่อนุญาตให้ใช้ถ่าน
  • ค่าจุดลอยตัวไม่สามารถแปลงเป็นชนิดจุดลอยตัวอื่นที่ไม่สามารถเก็บค่าได้ ตัวอย่างเช่นอนุญาตให้ใช้ float เพื่อ double แต่ไม่อนุญาตให้ double ลอย
  • ค่า floating-point ไม่สามารถแปลงเป็นชนิดจำนวนเต็มได้
  • ค่าจำนวนเต็มไม่สามารถแปลงเป็นชนิดทศนิยมได้

ตัวอย่าง:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

เพียงสถานการณ์ที่ = เป็นที่ต้องการมากกว่า {} คือเมื่อใช้autoคำหลักที่จะได้รับชนิดที่กำหนดโดยการเริ่มต้น

ตัวอย่าง:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

ข้อสรุป

ต้องการการเริ่มต้น {} มากกว่าทางเลือกอื่นเว้นแต่คุณจะมีเหตุผลที่ดีที่จะไม่ทำ


52
นอกจากนี้ยังมีข้อเท็จจริงที่ว่าการใช้()สามารถแยกวิเคราะห์เป็นการประกาศฟังก์ชัน มันเป็นเรื่องที่ทำให้เกิดความสับสนและไม่สอดคล้องกันที่คุณสามารถพูดได้แต่ไม่T t(x,y,z); T t()และบางครั้งคุณแน่ใจxคุณไม่สามารถพูดT t(x);ได้
juanchopanza

84
ฉันไม่เห็นด้วยอย่างยิ่งกับคำตอบนี้ เริ่มต้นยันกลายเป็นระเบียบสมบูรณ์เมื่อคุณมีประเภทด้วย ctor std::initializer_listยอมรับ RedXIII กล่าวถึงปัญหานี้ (และเพิ่งแปรงออก) ในขณะที่คุณไม่สนใจมันอย่างสมบูรณ์ A(5,4)และA{5,4}สามารถเรียกใช้ฟังก์ชั่นที่แตกต่างอย่างสิ้นเชิงและนี่คือสิ่งสำคัญที่ต้องรู้ มันสามารถส่งผลให้สายที่ดูเหมือนไม่ได้ใช้งานง่าย การบอกว่าคุณควรชอบ{}โดยปริยายจะทำให้คนเข้าใจผิดว่าเกิดอะไรขึ้น นี่ไม่ใช่ความผิดของคุณ โดยส่วนตัวฉันคิดว่ามันเป็นคุณสมบัติที่คิดออกมาแย่มาก
user1520427

13
@ user1520427 นั่นเป็นเหตุผลที่มีว่า " ถ้าคุณมีเหตุผลที่ดีที่จะไม่ " ส่วนหนึ่ง
Oleksiy

67
แม้ว่าคำถามนี้จะเก่า แต่ก็มีการตีค่อนข้างน้อยดังนั้นฉันจึงเพิ่มนี่เพื่ออ้างอิงเท่านั้น (ฉันไม่ได้เห็นที่อื่นในหน้านี้) จาก C ++ 14 กับใหม่กฎสำหรับการหักเงินอัตโนมัติจากยัน-init รายการตอนนี้มันเป็นไปได้ที่จะเขียนauto var{ 5 }และมันจะถูกอนุมานเป็นไม่มากเป็นint std::initializer_list<int>
Edoardo Sparkon Dominici

12
ฮ่าฮ่าจากความคิดเห็นทั้งหมดมันยังไม่ชัดเจนว่าจะทำอย่างไร สิ่งที่ชัดเจนคือข้อกำหนด C + + เป็นระเบียบ!
DrumM

113

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

  • หากวัตถุที่ฉันสร้างอยู่ในแนวความคิดถือค่าที่ฉันส่งผ่านในคอนสตรัค (เช่นคอนเทนเนอร์ POD structs อะตอมมิกพอยน์สมาร์ทพอยน์เตอร์ ฯลฯ ) จากนั้นฉันก็ใช้เหล็กดัดฟัน
  • หากคอนสตรัคคล้ายกับการเรียกใช้ฟังก์ชั่นปกติ (มันทำการดำเนินการที่ซับซ้อนมากขึ้นหรือน้อยลงที่ parametrized โดยข้อโต้แย้ง) จากนั้นฉันใช้ไวยากรณ์การเรียกฟังก์ชั่นปกติ
  • สำหรับการเริ่มต้นฉันมักจะใช้เครื่องมือจัดฟันเป็นลอน
    สำหรับวิธีนี้ฉันมั่นใจเสมอว่าวัตถุนั้นได้รับการเริ่มต้นโดยไม่คำนึงว่ามันเป็นคลาส "ของจริง" ที่มีคอนสตรัคเตอร์เริ่มต้นที่จะได้รับการเรียกว่าต่อไปหรือชนิด builtin / POD ประการที่สองคือ - ในกรณีส่วนใหญ่ - สอดคล้องกับกฎข้อแรกเป็นวัตถุเริ่มต้นเริ่มต้นมักจะหมายถึงวัตถุ "ว่าง"

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

มันเหมาะอย่างยิ่งกับประเภทห้องสมุดมาตรฐานเช่นstd::vector:

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements

11
เห็นด้วยอย่างยิ่งกับคำตอบส่วนใหญ่ของคุณ อย่างไรก็ตามคุณไม่คิดหรือว่าการใส่วงเล็บปีกกาเปล่าสำหรับเวกเตอร์นั้นซ้ำซ้อน? ฉันหมายความว่ามันก็โอเคเมื่อคุณต้องการกำหนดค่าเริ่มต้นวัตถุประเภททั่วไป T แต่วัตถุประสงค์ของการทำมันสำหรับรหัสที่ไม่ใช่ทั่วไปคืออะไร?
Mikhail

8
@ Mikail: มันซ้ำซ้อนแน่นอน แต่มันเป็นนิสัยของฉันที่จะทำให้การเริ่มต้นตัวแปรท้องถิ่นชัดเจน อย่างที่ฉันเขียนนี่เป็นเรื่องเกี่ยวกับความยินยอมเป็นหลักดังนั้นฉันจึงไม่ลืมเมื่อมันเป็นเรื่องสำคัญ มันไม่มีอะไรแน่นอนที่ฉันจะพูดถึงในการตรวจสอบรหัสหรือใส่ในคู่มือสไตล์ว่า
MikeMB

4
ruleset สะอาดสวย
laike9m

5
นี่คือคำตอบที่ดีที่สุด {} เป็นเหมือนการสืบทอด - ง่ายต่อการละเมิดนำไปสู่การเข้าใจรหัสยาก
UKMonkey

2
@MikeMB ตัวอย่าง: const int &b{}<- ไม่พยายามสร้างการอ้างอิงที่ไม่ใช่ค่าเริ่มต้น แต่ผูกกับวัตถุจำนวนเต็มชั่วคราว ตัวอย่างที่สอง: struct A { const int &b; A():b{} {} };<- ไม่พยายามสร้างการอ้างอิงที่ไม่มีค่าเริ่มต้น (เช่นเดียวกับที่()ทำ) แต่ผูกกับวัตถุจำนวนเต็มชั่วคราวจากนั้นปล่อยให้มันห้อยลงมา GCC ถึงกับ-Wallไม่เตือนสำหรับตัวอย่างที่สอง
Johannes Schaub - litb

91

มีเหตุผลมากมายที่จะใช้การเตรียมใช้งานวงเล็บปีกกา แต่คุณควรระวังว่าตัวinitializer_list<>สร้างต้องการให้ตัวสร้างอื่น ๆยกเว้นว่าเป็นตัวสร้างเริ่มต้น สิ่งนี้นำไปสู่ปัญหากับตัวสร้างและแม่แบบที่ตัวTสร้างประเภทสามารถเป็นรายการเริ่มต้นหรือ ctor เก่าธรรมดา

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

สมมติว่าคุณไม่พบคลาสดังกล่าวมีเหตุผลเพียงเล็กน้อยที่จะไม่ใช้รายการ intializer


20
นี่คือจุดสำคัญมากในการเขียนโปรแกรมทั่วไป เมื่อคุณเขียนเทมเพลตอย่าใช้ braced-init-list (ชื่อมาตรฐานสำหรับ{ ... }) เว้นแต่ว่าคุณต้องการซีแมนทิกส์initializer_list(ดีและอาจเป็นค่าเริ่มต้นสำหรับการสร้างวัตถุ)
Xeo

82
ฉันไม่เข้าใจว่าทำไมstd::initializer_listกฎถึงมีอยู่จริง - มันเพิ่มความสับสนและสับสนให้กับภาษา มีอะไรผิดปกติกับการทำFoo{{a}}ถ้าคุณต้องการตัวstd::initializer_listสร้าง? ดูเหมือนว่าจะเข้าใจได้ง่ายกว่าการstd::initializer_listให้ความสำคัญมากกว่าโอเวอร์โหลดอื่น ๆ ทั้งหมด
user1520427

5
+1 สำหรับความคิดเห็นข้างต้นเพราะมันยุ่งจริงๆฉันคิดว่า !! มันไม่ใช่ตรรกะ Foo{{a}}ตามตรรกะบางอย่างสำหรับฉันมากกว่าFoo{a}ที่จะกลายเป็นความสำคัญของรายการ intializer (ผู้ใช้อาจคิดว่า hm ... )
Gabriel

32
โดยพื้นฐาน C ++ 11 จะแทนที่หนึ่งระเบียบด้วยระเบียบอื่น โอ้ขอโทษที่มันไม่ได้แทนที่ - มันเพิ่มเข้าไป คุณจะรู้ได้อย่างไรว่าคุณไม่พบคลาสดังกล่าว ถ้าคุณเริ่มโดยไม่มีตัว std::initializer_list<Foo>สร้าง แต่มันจะถูกเพิ่มเข้าไปในFooชั้นเรียนในบางจุดเพื่อขยายส่วนต่อประสาน จากนั้นผู้ใช้ในFooชั้นเรียนจะเมาขึ้น
doc

10
.. "เหตุผลมากมายที่จะใช้การเริ่มต้นรั้ง" คืออะไร คำตอบนี้ชี้ให้เห็นเหตุผลหนึ่งข้อ ( initializer_list<>) ซึ่งไม่ผ่านการพิจารณาว่าใครเป็นคนเลือกแล้วจากนั้นก็พูดถึงกรณีที่ดีอีกข้อหนึ่งที่ไม่ต้องการ ฉันพลาดอะไรไป ~ 30 คน (ณ วันที่ 2016-04-21) พบว่ามีประโยชน์
dwanderson

0

ปลอดภัยกว่าตราบใดที่คุณไม่สร้างด้วย - ไม่ จำกัด ขอบเขตอย่างที่ Google พูดใน Chromium หากคุณทำเช่นนั้นจะปลอดภัยน้อยกว่า หากไม่มีการตั้งค่าสถานะนั้นกรณีที่ไม่ปลอดภัยเท่านั้นที่จะได้รับการแก้ไขภายใน C ++ 20

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

ทั้งสองรวมกันหมายความว่าพวกเขาปลอดภัยถ้าสิ่งที่อยู่ภายในเป็นค่าคงที่ดั้งเดิม แต่ปลอดภัยน้อยกว่าถ้าพวกเขาเป็นวัตถุ (แม้ว่าแก้ไขใน C ++ 20)


ฉันลองใช้ noodling รอบ ๆ บน goldbolt.org เพื่อหลีกเลี่ยง Constructor "ชัดแจ้ง" หรือ "ส่วนตัว" โดยใช้โค้ดตัวอย่างที่ให้มาและสร้างหนึ่งหรืออื่น ๆ ส่วนตัวหรือชัดเจนและได้รับรางวัลกับข้อผิดพลาดคอมไพเลอร์ที่เหมาะสม [s] สนใจการสำรองข้อมูลด้วยโค้ดตัวอย่างบ้างหรือไม่
Mark Storer

นี่คือการแก้ไขปัญหาที่เสนอสำหรับ C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen

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