การเรียก constructors ใน c ++ โดยไม่มีสิ่งใหม่


142

ฉันมักจะเห็นว่าผู้คนสร้างวัตถุใน C ++ โดยใช้

Thing myThing("asdf");

แทนสิ่งนี้:

Thing myThing = Thing("asdf");

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


25
ไม่ว่ารูปแบบใดจะใหม่
Daniel Daranas

13
รูปแบบที่สองจะใช้ตัวสร้างการคัดลอกดังนั้นไม่พวกเขาจะไม่เทียบเท่า
Edward Strange

ผมเล่นบิตกับมันวิธีแรกดูเหมือนว่าจะล้มเหลวในบางครั้งเมื่อแม่จะใช้กับการก่อสร้าง parameterless ..
นิลส์

1
Ouh และฉันได้รับตรา "คำถามที่ดี" สำหรับเรื่องนี้ช่างเป็นเรื่องน่าละอาย!
นิลส์

คำตอบ:


153

ความจริงแล้วทั้งสองบรรทัดนั้นถูกต้อง แต่ทำสิ่งต่าง ๆ อย่างละเอียด

บรรทัดแรกสร้างวัตถุใหม่ใน stack Thing(const char*)โดยการเรียกตัวสร้างรูปแบบที่

อันที่สองค่อนข้างซับซ้อนกว่าเล็กน้อย มันทำสิ่งต่อไปนี้เป็นหลัก

  1. สร้างวัตถุประเภทThingโดยใช้ตัวสร้างThing(const char*)
  2. สร้างวัตถุประเภทThingโดยใช้ตัวสร้างThing(const Thing&)
  3. โทรหา~Thing()วัตถุที่สร้างขึ้นในขั้นตอนที่ # 1

7
ฉันเดาว่าการกระทำประเภทนี้ได้รับการปรับให้เหมาะสมที่สุดดังนั้นจึงไม่แตกต่างกันในด้านประสิทธิภาพ
เอ็ม. วิลเลียมส์

14
ฉันไม่คิดว่าขั้นตอนของคุณค่อนข้างถูกต้อง Thing myThing = Thing(...)ไม่ได้ใช้ตัวดำเนินการที่ได้รับมอบหมายมันยังคงสร้างสำเนาเหมือนที่พูดThing myThing(Thing(...))และไม่เกี่ยวข้องกับค่าเริ่มต้นที่สร้างThing(แก้ไข: โพสต์ถูกแก้ไขภายหลัง)
AshleysBrain

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

3
ไม่ @Jared ไม่รับประกัน แต่แม้ว่าคอมไพเลอร์เลือกที่จะทำการปรับให้เหมาะสมนั้นตัวสร้างการคัดลอกยังคงต้องสามารถเข้าถึงได้ (เช่นไม่ได้รับการป้องกันหรือเป็นส่วนตัว) แม้ว่าจะไม่ได้ใช้งานหรือถูกเรียก
Rob Kennedy

3
ดูเหมือนว่าการคัดลอกสามารถแก้ไขแม้ว่าตัวสร้างการคัดลอกมีผลข้างเคียง - ดูคำตอบของฉัน: stackoverflow.com/questions/2722879//
ดักลาส Leeder

31

ฉันสมมติกับบรรทัดที่สองที่คุณหมายถึงจริง:

Thing *thing = new Thing("uiae");

ซึ่งจะเป็นวิธีมาตรฐานในการสร้างวัตถุแบบไดนามิกใหม่(จำเป็นสำหรับการเชื่อมโยงแบบไดนามิกและความหลากหลาย) และการจัดเก็บที่อยู่ของพวกเขาไปยังตัวชี้ รหัสของคุณทำในสิ่งที่ JaredPar อธิบายไว้นั่นคือการสร้างวัตถุสองรายการ (อันหนึ่งผ่านไปconst char*อีกอันผ่านไปอีกหนึ่งรายการconst Thing&) จากนั้นเรียก destructor ( ~Thing()) บนวัตถุแรก ( const char*หนึ่งรายการ)

ในทางตรงกันข้ามสิ่งนี้:

Thing thing("uiae");

สร้างวัตถุคงที่ซึ่งถูกทำลายโดยอัตโนมัติเมื่อออกจากขอบเขตปัจจุบัน


1
น่าเสียดายที่เป็นวิธีที่พบได้บ่อยที่สุดในการสร้างวัตถุแบบไดนามิกใหม่แทนที่จะใช้ auto_ptr, unique_ptr หรือเกี่ยวข้อง
Fred Nurk

3
คำถามของ OP นั้นถูกต้องคำตอบนี้เกี่ยวข้องกับปัญหาอื่นทั้งหมด (ดู @ คำตอบของ @ JaredPar)
Silmathoron

21

คอมไพเลอร์อาจปรับรูปแบบที่สองให้เหมาะสมในรูปแบบแรก แต่ไม่จำเป็นต้องทำ

#include <iostream>

class A
{
    public:
        A() { std::cerr << "Empty constructor" << std::endl; }
        A(const A&) { std::cerr << "Copy constructor" << std::endl; }
        A(const char* str) { std::cerr << "char constructor: " << str << std::endl; }
        ~A() { std::cerr << "destructor" << std::endl; }
};

void direct()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void assignment()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a = A(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void prove_copy_constructor_is_called()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    A b = a;
    static_cast<void>(b); // avoid warnings about unused variables
}

int main()
{
    direct();
    assignment();
    prove_copy_constructor_is_called();
    return 0;
}

เอาต์พุตจาก gcc 4.4:

TEST: direct
char constructor: direct
destructor

TEST: assignment
char constructor: assignment
destructor

TEST: prove_copy_constructor_is_called
char constructor: prove_copy_constructor_is_called
Copy constructor
destructor
destructor

อะไรคือจุดประสงค์ของการคงการปลดเปลื้องให้เป็นโมฆะ?
Stephen Cross

1
@Stephen หลีกเลี่ยงคำเตือนเกี่ยวกับตัวแปรที่ไม่ได้ใช้
Douglas Leeder

10

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


สำหรับผู้ที่ไม่คุ้นเคยกับความแตกต่างระหว่างการสร้างอินสแตนซ์วัตถุบนสแต็กซึ่งตรงข้ามกับฮีป (นั่นคือการใช้ใหม่และไม่ได้ใช้ใหม่ ) นี่เป็นเธรดที่ดี
edmqkk

2

เป็นการดีที่คอมไพเลอร์จะเพิ่มประสิทธิภาพที่สอง แต่ไม่จำเป็น วิธีแรกคือวิธีที่ดีที่สุด อย่างไรก็ตามมันค่อนข้างสำคัญที่จะต้องเข้าใจความแตกต่างระหว่างสแต็คและฮีปใน C ++ เพราะคุณต้องจัดการหน่วยความจำฮีปของคุณเอง


คอมไพเลอร์สามารถรับประกันได้หรือไม่ว่าตัวสร้างการคัดลอกไม่มีผลข้างเคียง (เช่น I / O)
Stephen Cross

@Stephen - ไม่สำคัญว่าตัวสร้างการคัดลอกทำ I / O - ดูคำตอบของฉันstackoverflow.com/questions/2722879/…
Douglas Leeder

ตกลงฉันเห็นแล้วคอมไพเลอร์ได้รับอนุญาตให้เปลี่ยนรูปแบบที่สองเป็นแบบแรกและหลีกเลี่ยงการเรียกไปที่ตัวสร้างการคัดลอก
Stephen Cross

2

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

#include <iostream> 

using namespace std;

class Thing
{
public:
    Thing();
};

Thing::Thing()
{
    cout << "Hi" << endl;
}

int main()
{
    //Thing myThing(); // Does not work
    Thing myThing; // Works

}

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


6
นี่เป็นความคลุมเครือทางไวยากรณ์ที่รู้จักกันดีใน C ++ เมื่อคุณเขียน "int rand ()" คอมไพเลอร์ไม่สามารถรู้ได้ว่าคุณหมายถึง "สร้าง int และเริ่มต้นเริ่มต้นมัน" หรือ "ประกาศฟังก์ชั่นแรนด์" กฎคือมันเลือกหลังเมื่อทำได้
jpalecek

1
และสิ่งนี้ผู้คนเป็นที่แจงที่สุด
Marc.2377

2

ต่อท้ายคำตอบJaredPar

ctor 1 ปกติ, 2- ฟังก์ชั่นเหมือน ctor กับวัตถุชั่วคราว

รวบรวมแหล่งนี้ที่ใดที่หนึ่งที่นี่http://melpon.org/wandbox/ด้วยคอมไพเลอร์ที่แตกต่างกัน

// turn off rvo for clang, gcc with '-fno-elide-constructors'

#include <stdio.h>
class Thing {
public:
    Thing(const char*){puts(__FUNCTION__ );}
    Thing(const Thing&){puts(__FUNCTION__ );}   
    ~Thing(){puts(__FUNCTION__);}
};
int main(int /*argc*/, const char** /*argv*/) {
    Thing myThing = Thing("asdf");
}

และคุณจะเห็นผล

จาก ISO / IEC 14882 2003-10-15

8.5 ตอนที่ 12

การก่อสร้างที่ 1 และ 2 ของคุณเรียกว่าการเริ่มต้นโดยตรง

12.1 ตอนที่ 13

การแปลงประเภทสัญกรณ์ทำงาน (5.2.3) สามารถใช้เพื่อสร้างออบเจ็กต์ใหม่ของประเภทนั้น [หมายเหตุ: ไวยากรณ์ดูเหมือนว่าจะมีการเรียกตัวสร้างอย่างชัดเจน ] ... วัตถุที่สร้างในลักษณะนี้ไม่มีชื่อ [หมายเหตุ: 12.2 อธิบายอายุการใช้งานของวัตถุชั่วคราว ] [หมายเหตุ: การเรียกตัวสร้างอย่างชัดเจนไม่ได้ให้ค่า lvalues ​​ดู 3.10 ]


จะอ่านเกี่ยวกับ RVO ได้ที่ไหน:

12 ฟังก์ชั่นสมาชิกพิเศษ / 12.8 การคัดลอกคลาสอ็อบเจ็กต์ / ส่วนที่ 15

เมื่อเกณฑ์ที่แน่นอนจะได้พบกับการดำเนินการที่ได้รับอนุญาตที่จะละเว้นการก่อสร้างสำเนาของวัตถุชั้นแม้กระทั่งถ้านวกรรมิกสำเนาและ / หรือ destructor สำหรับวัตถุมีผลข้างเคียง

ปิดโดยใช้ธงคอมไพเลอร์จากความคิดเห็นเพื่อดูพฤติกรรมการทำสำเนา)

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