เมื่อใดควรใช้ตัวเริ่มต้นที่ปิดด้วยปีกกา


97

ใน C ++ 11 เรามีไวยากรณ์ใหม่สำหรับการเริ่มต้นคลาสซึ่งทำให้เรามีความเป็นไปได้มากมายในการเริ่มต้นตัวแปร

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

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

เมื่อพูดถึงรหัสเทมเพลตการเปลี่ยนไวยากรณ์อาจทำให้เกิดความหมายที่แตกต่างกันดังนั้นการไปในทางที่ถูกต้องจึงเป็นสิ่งสำคัญ

ฉันสงสัยว่ามีแนวทางสากลที่ควรเลือกไวยากรณ์หรือไม่


1
ตัวอย่างพฤติกรรมที่ไม่ได้ตั้งใจจาก {} initialisation: string (50, 'x') vs string {50, 'x'} ที่นี่
P i

คำตอบ:


65

ฉันคิดว่าสิ่งต่อไปนี้อาจเป็นแนวทางที่ดี:

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

  • หากค่าที่คุณกำลังเริ่มต้นเป็นรายการของค่าที่จะเก็บไว้ในออบเจ็กต์ (เช่นองค์ประกอบของเวกเตอร์ / อาร์เรย์หรือส่วนจริง / จินตภาพของจำนวนเชิงซ้อน) ให้ใช้การเริ่มต้นวงเล็บปีกกาถ้ามี

  • หากค่าที่คุณกำลังเตรียมใช้งานไม่ใช่ค่าที่จะจัดเก็บ แต่อธิบายถึงค่า / สถานะของวัตถุที่ต้องการให้ใช้วงเล็บ ตัวอย่างคืออาร์กิวเมนต์ขนาดของ a vectorหรืออาร์กิวเมนต์ชื่อไฟล์ของไฟล์fstream.


4
@ user1304032: โลแคลไม่ใช่สตริงดังนั้นคุณจะไม่ใช้การเริ่มต้นการคัดลอก โลแคลยังไม่มีสตริง (อาจเก็บสตริงนั้นไว้เป็นรายละเอียดการใช้งาน แต่นั่นไม่ใช่จุดประสงค์) ดังนั้นคุณจะไม่ใช้การกำหนดค่าเริ่มต้นของวงเล็บปีกกา ดังนั้นแนวทางจึงบอกว่าให้ใช้การเริ่มต้นในวงเล็บ
celtschk

2
โดยส่วนตัวแล้วฉันชอบแนวทางนี้มากที่สุดและมันก็ใช้ได้ดีกับโค้ดทั่วไป มีข้อยกเว้นบางประการ ( T {}หรือเหตุผลทางวากยสัมพันธ์เช่นการแยกวิเคราะห์ที่น่ารำคาญที่สุด ) แต่โดยทั่วไปแล้วฉันคิดว่านี่เป็นคำแนะนำที่ดี โปรดทราบว่านี่เป็นความคิดเห็นส่วนตัวของฉันดังนั้นเราควรดูคำตอบอื่น ๆ ด้วย
helami

2
@celtschk: จะใช้ไม่ได้กับประเภทที่ไม่สามารถคัดลอกได้และไม่สามารถเคลื่อนย้ายได้ type var{};ทำ.
ildjarn

2
@celtschk: ฉันไม่ได้บอกว่ามันเป็นสิ่งที่จะเกิดขึ้นบ่อย แต่มันพิมพ์น้อยลงและทำงานในบริบทที่มากขึ้นดังนั้นข้อเสียคืออะไร?
ildjarn

2
แนวทางของฉันไม่เคยเรียกร้องให้คัดลอกเริ่มต้นอย่างแน่นอน ; -]
ildjarn

27

ฉันค่อนข้างมั่นใจว่าจะไม่มีแนวทางสากล วิธีการของฉันคือใช้วงเล็บปีกกาจำไว้เสมอ

  1. ตัวสร้างรายการตัวเริ่มต้นมีความสำคัญเหนือตัวสร้างอื่น ๆ
  2. คอนเทนเนอร์ไลบรารีมาตรฐานและ std :: basic_string ทั้งหมดมีตัวสร้างรายการตัวเริ่มต้น
  3. การเริ่มต้น Curly brace ไม่อนุญาตให้ จำกัด การแปลง

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


7
การจัดฟันแบบหยิกมีข้อเสียที่ฉันสามารถเรียกตัวสร้างรายการได้โดยไม่ได้ตั้งใจ วงเล็บกลมไม่ได้ นั่นไม่ใช่เหตุผลที่จะใช้วงเล็บกลมโดยค่าเริ่มต้นหรือไม่?
helami

4
@ ผู้ใช้: int i = 0;ฉันไม่คิดว่าจะint i{0}มีใครใช้ที่นั่นและมันอาจจะสับสน (เช่นกัน0คือถ้าพิมพ์intดังนั้นจะไม่มีการจำกัด ) สำหรับอย่างอื่นฉันจะทำตามคำแนะนำของ Juancho: ชอบ {} ระวังบางกรณีที่คุณไม่ควรทำ โปรดทราบว่ามีไม่กี่ประเภทที่จะใช้รายการตัวเริ่มต้นเป็นอาร์กิวเมนต์ตัวสร้างคุณสามารถคาดหวังว่าคอนเทนเนอร์และประเภทที่เหมือนคอนเทนเนอร์ (ทูเพิล ... ) จะมีอยู่ แต่โค้ดส่วนใหญ่จะเรียกตัวสร้างที่เหมาะสม
David Rodríguez - dribeas

3
@ user1304032 ขึ้นอยู่กับว่าคุณสนใจการ จำกัด ฉันชอบดังนั้นฉันจึงชอบให้คอมไพเลอร์บอกว่าint i{some floating point}เป็นข้อผิดพลาดแทนที่จะตัดทอนแบบเงียบ ๆ
juanchopanza

3
เกี่ยวกับ "ชอบ {} โปรดระวังบางกรณีที่คุณไม่ควร": สมมติว่าสองคลาสมีตัวสร้างที่เทียบเท่าความหมาย แต่คลาสหนึ่งมีรายการตัวเริ่มต้นด้วย ควรเรียกตัวสร้างที่เทียบเท่ากันสองตัวแตกต่างกันหรือไม่?
helami

3
@helami: "สมมติว่าสองคลาสมีตัวสร้างที่เทียบเท่าความหมาย แต่คลาสหนึ่งมีรายการตัวเริ่มต้นด้วยเช่นกันควรเรียกตัวสร้างที่เท่ากันสองตัวแตกต่างกันหรือไม่" สมมติว่าฉันเจอการแยกวิเคราะห์ที่น่ารำคาญที่สุด ที่สามารถเกิดขึ้นได้ที่ใดคอนสตรัคเช่นใด มันง่ายมากที่จะหลีกเลี่ยงปัญหานี้ถ้าคุณเพียงแค่ใช้{}หมายถึง "การเริ่มต้น" ถ้าคุณอย่างไม่สามารถ
Nicol Bolas

16

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

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

หรือสำหรับอาร์กิวเมนต์ของฟังก์ชัน:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

สำหรับตัวแปรที่ฉันไม่ได้ให้ความสนใจมากนักระหว่างรูปแบบT t = { init };หรือT t { init };ฉันพบว่าความแตกต่างเป็นเพียงเล็กน้อยและอย่างแย่ที่สุดก็จะส่งผลให้มีข้อความคอมไพเลอร์ที่เป็นประโยชน์เกี่ยวกับการใช้ตัวexplicitสร้างในทางที่ผิด

สำหรับประเภทที่ยอมรับstd::initializer_listแม้ว่าจะเห็นได้ชัดว่าบางครั้งไม่std::initializer_listจำเป็นต้องใช้ตัวสร้าง (ตัวอย่างคลาสสิกคือstd::vector<int> twenty_answers(20, 42);) ไม่ต้องใช้เครื่องมือจัดฟันแล้ว


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

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

จากนั้นauto p = make_unique<std::vector<T>>(20, T {});สร้างเวกเตอร์ขนาด 2 ถ้าTเป็นเช่นintหรือเวกเตอร์ขนาด 20 ถ้าเป็นT std::stringสัญญาณที่บอกได้ชัดเจนว่ามีบางอย่างผิดปกติเกิดขึ้นที่นี่คือไม่มีลักษณะใดที่สามารถช่วยคุณได้ที่นี่ (เช่นกับ SFINAE): std::is_constructibleเป็นในแง่ของการเริ่มต้นโดยตรงในขณะที่เราใช้การเริ่มต้นด้วยรั้งซึ่งเลื่อนไปตรง - การเริ่มต้นในกรณีที่ไม่มีตัวสร้างที่std::initializer_listรบกวน ในทำนองเดียวกันstd::is_convertibleไม่มีความช่วยเหลือ

ฉันได้ตรวจสอบแล้วว่าเป็นไปได้หรือไม่ที่จะม้วนลักษณะที่สามารถแก้ไขได้ แต่ฉันไม่ได้มองโลกในแง่ดีเกินไป ไม่ว่าในกรณีใดฉันไม่คิดว่าเราจะพลาดอะไรไปมากฉันคิดว่าความจริงที่ว่าmake_unique<T>(foo, bar)ผลลัพธ์ในการก่อสร้างเทียบเท่าT(foo, bar)นั้นใช้งานง่ายมาก โดยเฉพาะอย่างยิ่งที่make_unique<T>({ foo, bar })เป็นที่แตกต่างกันมากและมี แต่จะทำให้รู้สึกว่าfooและbarมีชนิดเดียวกัน

ดังนั้นสำหรับรหัสทั่วไปฉันเท่านั้นที่ใช้สำหรับการจัดฟันเริ่มต้นค่า (เช่นT t {};หรือT t = {};) ซึ่งมีความสะดวกมากและฉันคิดว่าดีกว่าไปที่ C ++ 03 T t = T();วิธี มิฉะนั้นจะเป็นไวยากรณ์การเริ่มต้นโดยตรง (เช่นT t(a0, a1, a2);) หรือบางครั้งโครงสร้างเริ่มต้น ( T t; stream >> t;เป็นกรณีเดียวที่ฉันคิดว่าฉันใช้)

นั่นไม่ได้หมายความว่าการจัดฟันทั้งหมดจะไม่ดีให้พิจารณาตัวอย่างก่อนหน้านี้ด้วยการแก้ไข:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

นี้ยังคงใช้เครื่องหมายวงเล็บสำหรับสร้างแม้ว่าชนิดที่เกิดขึ้นจริงขึ้นอยู่กับพารามิเตอร์แม่แบบstd::unique_ptr<T>T


@interjay บางตัวอย่างของฉันแน่นอนอาจจำเป็นต้องใช้ชนิดที่ไม่ได้ลงนามแทนเช่นmake_unique<T>(20u, T {})สำหรับTการเป็นอย่างใดอย่างหนึ่งหรือunsigned std::stringไม่แน่ใจในรายละเอียดมากเกินไป (โปรดทราบว่าฉันยังแสดงความคิดเห็นเกี่ยวกับความคาดหวังเกี่ยวกับการเริ่มต้นโดยตรงเทียบกับการเริ่มต้นของรั้งที่เกี่ยวข้องกับเช่นฟังก์ชันการส่งต่อที่สมบูรณ์แบบ) std::string c("qux");ไม่ได้ระบุให้ทำงานเป็นการเริ่มต้นในคลาสเพื่อหลีกเลี่ยงความคลุมเครือด้วยการประกาศฟังก์ชันสมาชิกในไวยากรณ์
Luc Danton

@interjay ฉันไม่เห็นด้วยกับคุณในข้อแรกอย่าลังเลที่จะตรวจสอบ 8.5.4 การเริ่มต้นรายการและ 13.3.1.7 การเริ่มต้นโดยรายการเริ่มต้น ในส่วนที่สองคุณต้องพิจารณาสิ่งที่ฉันเขียนให้ละเอียดยิ่งขึ้น (ซึ่งเกี่ยวกับการเริ่มต้นในคลาส ) และ / หรือไวยากรณ์ C ++ (เช่นตัวประกาศสมาชิกซึ่งอ้างอิงถึงbrace-or-equal-initializer )
Luc Danton

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