การกำหนดค่าเริ่มต้นเครื่องแบบ C ++ 11 เป็นการแทนที่ไวยากรณ์แบบเก่าหรือไม่


172

ฉันเข้าใจว่าการกำหนดค่าเริ่มต้นที่เหมือนกันของ C ++ 11 แก้ปัญหาความคลุมเครือทางไวยากรณ์ในภาษา แต่ในการนำเสนอของ Bjarne Stroustrup (โดยเฉพาะอย่างยิ่งในระหว่างการพูดคุย GoingNative 2012) ตัวอย่างของเขาใช้ไวยากรณ์นี้ทันทีทุกครั้งที่เขาสร้างวัตถุ

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

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


นี่อาจเป็นหัวข้อที่ถูกกล่าวถึงใน Programmers.se ดูเหมือนจะเอนไปทางด้านอัตนัย
Nicol Bolas

6
@NicolBolas: ในทางกลับกันคำตอบที่ยอดเยี่ยมของคุณอาจเป็นตัวเลือกที่ดีมากสำหรับแท็ก c ++ - faq ฉันไม่คิดว่าเรามีคำอธิบายสำหรับโพสต์นี้มาก่อน
Matthieu M.

คำตอบ:


233

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

ลดขนาดชื่อซ้ำซ้อนให้เหลือน้อยที่สุด

พิจารณาสิ่งต่อไปนี้:

vec3 GetValue()
{
  return vec3(x, y, z);
}

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

vec3 GetValue()
{
  return {x, y, z};
}

ทุกอย่างทำงานได้

ดียิ่งขึ้นสำหรับฟังก์ชั่นการขัดแย้ง พิจารณาสิ่งนี้:

void DoSomething(const std::string &str);

DoSomething("A string.");

ใช้งานได้โดยไม่ต้องพิมพ์ชื่อเพราะstd::stringรู้วิธีสร้างตัวเองconst char*โดยปริยาย เยี่ยมมาก แต่ถ้าสตริงนั้นมาจากอะไรให้พูด RapidXML หรือสตริงลัวะ นั่นคือสมมุติว่าฉันรู้ความจริงของความยาวของสตริงที่อยู่ข้างหน้า std::stringคอนสตรัคที่ใช้จะต้องใช้ความยาวของสตริงถ้าฉันเพียงแค่ผ่านconst char*const char*

มีโอเวอร์โหลดที่ใช้ความยาวอย่างชัดเจนว่าเป็น DoSomething(std::string(strValue, strLen))แต่การที่จะใช้มันฉันต้องทำเช่นนี้: ทำไมต้องพิมพ์ชื่อพิเศษในนั้น คอมไพเลอร์รู้ว่าประเภทคืออะไร เช่นเดียวกับautoเราสามารถหลีกเลี่ยงการพิมพ์ชื่อพิเศษ:

DoSomething({strValue, strLen});

มันใช้งานได้ ไม่มีชื่อพิมพ์ไม่ต้องยุ่งยากอะไร คอมไพเลอร์ทำงานได้รหัสสั้นและทุกคนก็มีความสุข

ได้รับมีข้อโต้แย้งที่จะทำให้รุ่นแรก ( DoSomething(std::string(strValue, strLen))) ชัดเจนมากขึ้น นั่นคือมันชัดเจนว่าเกิดอะไรขึ้นและใครทำอะไร นั่นเป็นเรื่องจริง การทำความเข้าใจโค้ดที่ใช้การกำหนดค่าเริ่มต้นที่สม่ำเสมอนั้นต้องดูที่ต้นแบบฟังก์ชัน นี่คือเหตุผลเดียวกันว่าทำไมบางคนบอกว่าคุณไม่ควรผ่านพารามิเตอร์โดยการอ้างอิงที่ไม่ใช่ const: เพื่อให้คุณสามารถดูได้ที่ไซต์การโทรหากมีการแก้ไขค่า

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

ไม่เคยได้รับการแยกวิเคราะห์ Vexing มากที่สุด

นี่คือรหัสบางส่วน

class Bar;

void Func()
{
  int foo(Bar());
}

คำถามป๊อป: อะไรนะfoo? หากคุณตอบว่า "ตัวแปร" คุณผิด อันที่จริงมันเป็นต้นแบบของฟังก์ชันที่ใช้เป็นพารามิเตอร์ของฟังก์ชันที่ส่งคืน a Barและfooค่าส่งคืนของฟังก์ชันนั้นเป็น int

สิ่งนี้เรียกว่า "Most Vexing Parse" ของ C ++ เพราะมันไม่สมเหตุสมผลกับมนุษย์ แต่กฎของ C ++ น่าเศร้าต้องการสิ่งนี้ถ้ามันสามารถตีความได้ว่าเป็นฟังก์ชั่นต้นแบบแล้วมันจะเป็น ปัญหาคือBar(); นั่นอาจเป็นหนึ่งในสองสิ่ง อาจเป็นชื่อที่มีประเภทBarซึ่งหมายความว่ามันกำลังสร้างชั่วคราว Barหรือมันอาจจะเป็นฟังก์ชั่นที่จะไม่มีพารามิเตอร์และผลตอบแทนที่

การกำหนดค่าเริ่มต้นที่เหมือนกันไม่สามารถตีความได้ว่าเป็นฟังก์ชันต้นแบบ:

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{}สร้างชั่วคราวเสมอ int foo{...}สร้างตัวแปรเสมอ

มีหลายกรณีที่คุณต้องการใช้Typename()แต่ไม่สามารถทำได้เนื่องจากกฎการแยกวิเคราะห์ของ C ++ ด้วยTypename{}ไม่มีความกำกวม

เหตุผลที่ไม่ควร

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

int val{5.2};

ที่จะไม่คอมไพล์ คุณสามารถทำได้ด้วยการกำหนดค่าเริ่มต้นแบบเก่า แต่ไม่ใช่การกำหนดค่าเริ่มต้นที่สม่ำเสมอ

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

แน่นอนว่าบางคนอาจโต้แย้งว่ารหัสดังกล่าวไม่สมควรรวบรวม โดยส่วนตัวฉันเห็นด้วย; การกวดขันเป็นสิ่งที่อันตรายมากและอาจนำไปสู่พฤติกรรมที่ไม่พึงประสงค์ อาจเป็นการดีที่สุดที่จะแก้ไขปัญหาเหล่านั้นตั้งแต่เนิ่น ๆ อย่างน้อยที่สุดการ จำกัด ให้แคบลงแสดงให้เห็นว่าบางคนไม่ได้คิดมากเรื่องรหัส

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

มีอีกเหตุผลหนึ่งที่ไม่:

std::vector<int> v{100};

สิ่งนี้ทำอะไร มันสามารถสร้างรายการที่vector<int>มีค่าเริ่มต้นที่สร้างขึ้นหนึ่งร้อยรายการ หรืออาจสร้างvector<int>ด้วย 1 100รายการที่เป็นค่า ทั้งสองเป็นไปได้ในทางทฤษฎี

ในความเป็นจริงมันทำหลัง

ทำไม? รายการเริ่มต้นใช้ไวยากรณ์เดียวกับการเริ่มต้นเครื่องแบบ ดังนั้นจะต้องมีกฎบางอย่างเพื่ออธิบายสิ่งที่ต้องทำในกรณีที่มีความกำกวม กฎง่ายๆคือสวย: ถ้าคอมไพเลอร์สามารถใช้ตัวสร้างรายการ initializer กับรายการรั้ง-เริ่มต้นแล้วมันจะ นับตั้งแต่vector<int>มีการเริ่มต้นรายการคอนสตรัคที่ใช้เวลาinitializer_list<int>และ {100} อาจจะเป็นที่ถูกต้องinitializer_list<int>มันจึงต้องเป็น

เพื่อให้ได้รับการปรับขนาดตัวสร้างคุณต้องใช้แทน(){}

โปรดทราบว่าหากนี่เป็นvectorสิ่งที่ไม่สามารถแปลงเป็นจำนวนเต็มได้จะไม่เกิดขึ้น initializer_list จะไม่พอดีกับตัวสร้างรายการ initializer ของvectorประเภทนั้นและคอมไพเลอร์จะมีอิสระที่จะเลือกจากตัวสร้างอื่น ๆ


11
+1 ถูกจับ ฉันจะลบคำตอบของฉันเนื่องจากคุณตอบทุกจุดในรายละเอียดมากขึ้น
R. Martinho Fernandes

21
std::vector<int> v{100, std::reserve_tag};จุดสุดท้ายคือเหตุผลที่ผมต้องการ std::resize_tagในทำนองเดียวกันกับ ขณะนี้มีสองขั้นตอนในการจองพื้นที่เวกเตอร์
Xeo

6
@ NicolBolas - สองประเด็น: ฉันคิดว่าปัญหากับการแยกวิเคราะห์ที่รบกวนคือ foo () ไม่ใช่ Bar () ถ้าคุณทำint foo(10)เช่นนั้นคุณจะไม่พบปัญหาเดียวกันหรือไม่? ประการที่สองอีกเหตุผลที่จะไม่ใช้มันดูเหมือนจะเป็นปัญหามากกว่าวิศวกรรม แต่ถ้าเราสร้างวัตถุทั้งหมดของเราใช้{}แต่วันต่อมาฉันจะเพิ่มคอนสตรัคเตอร์สำหรับรายการเริ่มต้น? ตอนนี้คำสั่งการก่อสร้างของฉันทั้งหมดกลายเป็นคำสั่งรายการเริ่มต้น ดูเหมือนจะเปราะบางมากในแง่ของการฟื้นฟู ความคิดเห็นใด ๆ เกี่ยวกับเรื่องนี้?
void.pointer

7
@RobertDailey: "ถ้าคุณทำคุณint foo(10)จะไม่พบปัญหาเดียวกันหรือไม่?" หมายเลข 10 คือตัวอักษรจำนวนเต็มและตัวอักษรจำนวนเต็มไม่สามารถเป็นชื่อพิมพ์ได้ การแยกคำที่รบกวนนั้นมาจากความจริงที่Bar()อาจเป็นชื่อพิมพ์หรือค่าชั่วคราว นั่นคือสิ่งที่สร้างความคลุมเครือให้กับคอมไพเลอร์
Nicol Bolas

8
unpleasant behavior- มีคำศัพท์มาตรฐานใหม่ที่ควรจดจำ:>
sehe

64

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

ดูรหัสต่อไปนี้:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

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

และที่นี่อีกครั้งมันไม่ใช่เรื่องง่ายสำหรับคนที่อ่านรหัสเป็นครั้งแรกที่จะเข้าใจสิ่งที่ถูกสร้างขึ้นจริง:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

โค้ดด้านบนจะหยุดลงเมื่อมีคนตัดสินใจว่า DoSomething ควรสนับสนุนประเภทสตริงอื่น ๆ และเพิ่มการโอเวอร์โหลดนี้:

void DoSomething(const CoolStringType& str);

หาก CoolStringType มีคอนสตรัคเตอร์ที่ใช้ const char * และ size_t (เช่นเดียวกับ std :: string) ดังนั้นการเรียกใช้ DoSomething ({strValue, strLen}) จะทำให้เกิดข้อผิดพลาดที่กำกวม

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

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

ตัวอย่างการใช้ความคิด # 1:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

ตัวอย่างการใช้ความคิด # 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

ฉันคิดว่ามันเป็นสิ่งที่ไม่ดีที่มาตรฐานใหม่อนุญาตให้ผู้ใช้เริ่มต้นบันไดเช่นนี้:

Stairs s {2, 4, 6};

... เพราะนั่นทำให้ความหมายของผู้สร้างทำให้งงงวย การเริ่มต้นเช่นนั้นดูเหมือนความคิด # 1 แต่ไม่ใช่ มันไม่ได้เทค่าความสูงของค่าต่าง ๆ สามระดับเข้าไปในวัตถุแม้ว่ามันจะดูเหมือน และที่สำคัญกว่านั้นถ้ามีการตีพิมพ์ไลบรารี่ของ Stairs เหมือนด้านบนและโปรแกรมเมอร์ได้ใช้มันแล้วถ้าไลบรารีของ implementor นั้นเพิ่มตัวสร้าง initializer_list ไปที่ Stairs โค้ดทั้งหมดที่ใช้ Stairs กับ Uniform Initialization ไวยากรณ์กำลังจะแตก

ฉันคิดว่าชุมชน C ++ ควรเห็นด้วยกับข้อตกลงร่วมกันเกี่ยวกับวิธีการใช้ Uniform Initialization เช่นในการเริ่มต้นทั้งหมดหรืออย่างที่ฉันแนะนำอย่างยิ่งให้แยกแนวคิดทั้งสองของการเริ่มต้นและทำให้ชัดเจนถึงเจตนาของโปรแกรมเมอร์กับผู้อ่าน รหัส.


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

กล่าวว่าไวยากรณ์ที่คุณต้องการในการทำสำเนาคือ:

T var1;
T var2 (var1);

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

T var2 {var1}; // fails if T is std::array for example

48
หากคุณมี "<ล็อตและล็อตของรหัสที่นี่>" รหัสของคุณจะยากต่อการเข้าใจโดยไม่คำนึงถึงไวยากรณ์
kevin cline

8
นอกเหนือจาก IMO มันเป็นหน้าที่ของ IDE ของคุณที่จะแจ้งให้คุณทราบว่ามันกำลังกลับมา (เช่นผ่านการโฮเวอร์) แน่นอนถ้าคุณไม่ได้ใช้ IDE ที่คุณเอาภาระเมื่อตัวเอง :)
abergmeier

4
@ TomiT ฉันเห็นด้วยกับบางส่วนของสิ่งที่คุณพูด อย่างไรก็ตามในจิตวิญญาณเดียวกันautoกับการอภิปรายการประกาศเทียบกับประเภทที่ชัดเจนฉันขอโต้แย้งสำหรับความสมดุล: ชุดเริ่มต้นเครื่องแบบใหญ่ครั้งใหญ่ในสถานการณ์การเขียนโปรแกรมเมตาแม่แบบที่ประเภทมักจะค่อนข้างชัดเจนอยู่แล้ว มันจะหลีกเลี่ยงการทำยี้ยนซับซ้อนของคุณซ้ำ ๆ-> decltype(....)เพื่อใช้คาถาเช่นสำหรับเทมเพลตฟังก์ชั่นออนไลน์ที่ง่าย (ทำให้ฉันร้องไห้)
sehe

5
" แต่ไวยากรณ์ที่ใช้วงเล็บปีกกาไม่ทำงานหากพิมพ์ T เป็นผลรวม: " โปรดทราบว่านี่เป็นข้อบกพร่องที่รายงานในมาตรฐานมากกว่าพฤติกรรมที่คาดหวังไว้
Nicol Bolas

5
"ตอนนี้เขาจะต้องเลื่อนกลับไปที่ลายเซ็นฟังก์ชั่น" ถ้าคุณต้องเลื่อนฟังก์ชั่นของคุณใหญ่เกินไป
Miles Rout

-3

หากคอนสตรัคเตอร์ของคุณmerely copy their parametersในตัวแปรคลาสที่เกี่ยวข้องin exactly the same orderซึ่งพวกมันถูกประกาศในคลาสจากนั้นการใช้การกำหนดค่าเริ่มต้นในที่สุดจะเร็วขึ้น (แต่ก็อาจเหมือนกันอย่างแน่นอน) กว่าการเรียกคอนสตรัคเตอร์

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


2
ทำไมคุณถึงบอกว่ามันเร็วกว่ากัน?
jbcoe

สิ่งนี้ไม่ถูกต้อง struct X { int i; }; int main() { X x{42}; }มีความต้องการที่จะประกาศผู้สร้างไม่เป็น: นอกจากนี้ยังไม่ถูกต้องการกำหนดค่าเริ่มต้นที่สม่ำเสมอสามารถทำได้เร็วกว่าการกำหนดค่าเริ่มต้น
ทิม
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.