หลีกเลี่ยงการเรียก Constructor ของตัวแปรสมาชิก

ฉันมีคลาส C++ ต่อไปนี้:

// Header-File
class A
{
    public:
    A();

    private:
    B m_B;
    C m_C;
};

// cpp-File
A::A()
: m_B(1)
{
    m_B.doSomething();
    m_B.doMore();
    m_C = C(m_B.getSomeValue());
}

ตอนนี้ฉันต้องการ หลีกเลี่ยง class A เพื่อเรียกตัวสร้าง ใดๆ ของ C m_C เพราะในบรรทัดสุดท้ายของ A::A() ฉันจะเริ่มต้น m_C ด้วยตัวเองอยู่แล้ว เพราะฉันต้องเตรียม m_B ก่อน ฉันสามารถจัดเตรียมคอนสตรัคเตอร์เริ่มต้นที่ว่างเปล่าสำหรับ class B ได้ แต่นั่นไม่ใช่ความคิด

ฉันได้ลองเพิ่ม m_C(NULL) ในรายการเริ่มต้นของ A::A() แล้ว บางครั้งก็ได้ผล บางครั้งก็บอกว่าไม่มีตัวสร้างที่รับ NULL เป็นอาร์กิวเมนต์

แล้วฉันจะให้ m_C ยังไม่ได้เตรียมใช้งานได้อย่างไร ฉันรู้ว่าด้วยพอยน์เตอร์ m_C(NULL)-way ก็ใช้งานได้ และฉันไม่ต้องการจัดสรรมันแบบไดนามิกโดยใช้ new

ความคิดใด ๆ ที่ชื่นชม


person Atmocreations    schedule 20.10.2011    source แหล่งที่มา
comment
วิธีที่ชั่วร้ายคือการมี char m_C_data[sizeof(C)] เป็นสมาชิก จากนั้นทำตำแหน่งใหม่เมื่อเริ่มต้น วิธีนี้จะหลีกเลี่ยงการจัดสรรฮีป แต่คุณจะต้องแปลงให้เป็นประเภทที่ถูกต้องทุกครั้งที่คุณใช้ แน่นอนว่าต้องคำนึงถึงความซับซ้อนของประเภท C (เป็น POD, มี vftable, ‹สิ่งอื่นๆ ที่ฉันไม่รู้›..ฯลฯ)   -  person Akanksh    schedule 20.10.2011
comment
คลาส C มีคอนสตรัคเตอร์เริ่มต้นซึ่งไม่ทำอะไรเลยใช่หรือไม่ อย่างอื่นเขียนคอนสตรัคเตอร์อีกหนึ่งตัวซึ่งใช้ NULL เป็นข้อโต้แย้งและทำให้ชัดเจนว่าปลอดภัยและไม่ทำอะไรเลย   -  person DumbCoder    schedule 20.10.2011
comment
จนถึงขณะนี้ก็ไม่ได้ และถึงแม้ว่าฉันไม่ต้องการให้มันมีคอนสตรัคเตอร์เริ่มต้น แต่ดูเหมือนว่าฉันแค่ต้องเปลี่ยนมันให้มี   -  person Atmocreations    schedule 20.10.2011
comment
น่าเสียดายที่คำตอบที่ยอมรับนั้นไม่ถูกต้อง อย่าลืมเลื่อนลงนะครับ...   -  person sdgfsdh    schedule 03.01.2017


คำตอบ (9)


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

ความคิด:

  • C มีตัวสร้างที่ไม่ทำอะไรเลย
  • C มีวิธีการเริ่มต้นที่ทำให้คลาสใช้งานได้
  • C ติดตามว่าได้รับการเริ่มต้นอย่างถูกต้องหรือไม่ และส่งคืนข้อผิดพลาดที่เหมาะสมหากใช้โดยไม่เริ่มต้น
person Tobias Langner    schedule 20.10.2011
comment
คือ... ฉันไม่เห็นด้วยบางส่วน ในตัวสร้าง ฉันแน่ใจว่าสมาชิกทุกคนได้รับการเริ่มต้นอย่างถูกต้องเช่นกัน หากคุณมีพอยน์เตอร์ แสดงว่าไม่ได้เริ่มต้นอย่างถูกต้องเช่นกัน แต่ดูเหมือนว่าฉันต้องใช้วิธีอื่น ขอบคุณ! - person Atmocreations; 20.10.2011
comment
ตกลง. ฉันคิดว่าทางออกเดียวคือสร้างตัวสร้างเริ่มต้นที่ว่างเปล่า วิธีแก้ปัญหาที่เหลือดูเหมือนจะดี แต่ไม่เหมาะกับปัญหาปัจจุบันของฉัน ขอบคุณ! - person Atmocreations; 20.10.2011
comment
@Atmocreations: ตัวชี้เริ่มต้นอย่างถูกต้อง - ตามตัวสร้างเริ่มต้นของพอยน์เตอร์ โดยปกติแล้วจะไม่มีความหมาย - ถ้าคุณโชคดี มันจะเป็น 0 - person Tobias Langner; 20.10.2011

แล้วการใช้เทคนิคที่อธิบายไว้ใน QA นี้ล่ะ?

ป้องกันการเรียกไปยังคอนสตรัคเตอร์เริ่มต้นสำหรับอาร์เรย์ภายในคลาส

std::aligned_storage<sizeof(T[n]), alignof(T)>::type

หรือคุณสามารถลองใช้ union ก็ได้ AFAIK ยูเนี่ยนจะเริ่มต้นได้เฉพาะกับคอนสตรัคเตอร์ของสมาชิกที่มีชื่อคนแรกเท่านั้น< /ก>

ตัวอย่างเช่น,

union
{
   uint8_t _nothing = 0; 
   C c;
};

ตามมาตรฐานที่กล่าวถึงใน QA นั้น c จะถูกกำหนดค่าเริ่มต้นเป็นศูนย์ และตัวสร้างจะไม่ถูกเรียก

person eonil    schedule 28.04.2014
comment
นี่คือคำตอบที่ถูกต้อง คุณไม่จำเป็นต้อง _nothing; a union ของหนึ่งองค์ประกอบจะทำได้ - person sdgfsdh; 02.12.2016
comment
เนื่องจาก C++17 คุณสามารถใช้ std::optional ที่สะดวกกว่าสำหรับเรื่องนี้ได้ . ไม่จัดสรรหน่วยความจำที่จำเป็นแบบไดนามิก และเสนอ #emplace() สำหรับการสร้างแบบแทนที่ในภายหลัง และ #operator=() สำหรับการกำหนดที่ง่ายดาย - person Neonit; 27.07.2020

คุณไม่สามารถ.

ตัวแปรสมาชิกทั้งหมดจะถูกสร้างแบบเต็มเมื่อมีการป้อนบล็อคโค้ด construcotr ซึ่งหมายความว่าจะต้องมีการเรียกตัวสร้าง

แต่คุณสามารถแก้ไขข้อจำกัดนี้ได้

// Header-File
class A
{
    struct Initer
    {
         Initer(B& b)
             : m_b(b)
         {
             m_b.doSomething();
             m_b.doMore();
         }
         operator int()  // assuming getSomeValue() returns int.
         {
             return m_b.getSomeValue();
         }
         B& m_b;
    };
    public:
    A();

    private:   // order important.
    B m_B;
    C m_C;
};


// cpp-File
A::A()
: m_B(1)
, m_C(Initer(m_B))
{
}
person Martin York    schedule 20.10.2011
comment
คุณสามารถทำสิ่งที่คล้ายกันด้วยวิธี Init แบบคงที่ได้หรือไม่ เช่น. m_B(1), m_C(Init(m_B)). ฉันต้องการใส่สิ่งนั้นในคำตอบของฉัน แต่ฉันไม่แน่ใจว่ามันถูกกฎหมายหรือไม่ - person Merlyn Morgan-Graham; 20.10.2011
comment
@merlyn-morgan-graham: ใช่ คุณต้องกำหนดตัวดำเนินการแปลงที่จะแปลงออบเจ็กต์ประเภท Init ให้เป็นประเภท T (โดยที่ T เหมือนกับประเภทผลลัพธ์เป็น getSomeValue()) - person Martin York; 20.10.2011
comment
ฉันตั้งใจจะกำหนด Init เป็น: private: static int Init(B& b) { /* init here */ return b.getSomeValue(); } ดูเหมือนว่าความคิดเห็นของคุณในคำตอบอื่นอาจยืนยันได้ว่าวิธีนี้ใช้ได้ผล - person Merlyn Morgan-Graham; 20.10.2011
comment
@Merlyn Morgan-Graham: ใช่การใช้วิธีคงที่ส่วนตัวนั้นสะอาดกว่านี้ ฉันจะทำตามความคิดของวลาดตอนนี้ที่ได้เห็นมันแล้ว - person Martin York; 20.10.2011
comment
@LokiAstari: ขอบคุณ! ฉันชอบวิธีแก้ปัญหานี้ แม้ว่าฉันจะพบว่าการสร้างคอนสตรัคเตอร์ว่างสำหรับ C ทำได้ง่ายกว่า - person Atmocreations; 20.10.2011

ฉันไม่เห็นวิธีที่ดีในการบรรลุสิ่งที่คุณต้องการ นี่จะต้องเป็นวิธีแก้ปัญหา:

// Header-File
class A
{
    public:
    A();

    private:
    B m_B;
    C m_C;
    static int prepareC(B& b);
};

// cpp-File
A::A()
: m_B(1)
, m_C(prepareC(m_B))
{
}

int A::prepareC(B& b)
{
    b.doSomething();
    b.doMore();
    return b.getSomeValue();
}

โปรดตรวจสอบให้แน่ใจว่า m_B.doSomething(), m_B.doMore() และ m_B.getSomeValue() ไม่ได้สัมผัส m_C (ทางตรงหรือทางอ้อม)


ตามที่ @Tobias กล่าวถึงอย่างถูกต้อง โซลูชันนี้ขึ้นอยู่กับลำดับของการเริ่มต้น คุณต้องแน่ใจว่าคำจำกัดความของ m_B และ m_C อยู่ในลำดับนี้


อัปเดตโค้ดตามแนวคิดของ @ Loki

person Vlad    schedule 20.10.2011
comment
คุณต้องทราบว่าลักษณะการทำงานของสิ่งนี้ขึ้นอยู่กับลำดับที่ถูกต้องของคำจำกัดความของ m_B และ m_C มันใช้งานได้ตราบใดที่ m_C ถูกกำหนดหลังจาก m_B กรุณาแสดงความคิดเห็นดังกล่าว. ไม่อย่างนั้นฉันก็ชอบเวอร์ชันนี้ - person Tobias Langner; 20.10.2011
comment
+1: แม้ว่าฉันจะกำหนดให้ waitC() เป็นสมาชิกแบบคงที่และส่ง b เป็นพารามิเตอร์ - person Martin York; 20.10.2011
comment
ดูเหมือนเป็นไปได้สำหรับฉัน เหตุใดฉันจึงต้องแน่ใจว่าวิธีการของ m_B จะไม่แตะ m_C - person Atmocreations; 20.10.2011
comment
@Atmocreations: เพราะนี่จะเป็นพฤติกรรมที่ไม่ได้กำหนด: m_C ยังไม่ได้เตรียมใช้งาน! - person Vlad; 20.10.2011
comment
@Atmocreations: อย่าส่ง m_C เป็นพารามิเตอร์ไปยังตัวสร้างของ m_B และตรวจสอบให้แน่ใจว่าประกาศ m_C หลัง m_B ในการประกาศคลาส - person Martin York; 20.10.2011
comment
@Loki: มีความเป็นไปได้เล็กน้อยที่วิธีการของ m_B จะเข้าถึงอินสแตนซ์ของ A ที่กำลังสร้างอยู่ (เช่น: A ส่งผ่าน this ไปยังตัวสร้างของ m_B) ดังนั้นควรตรวจสอบอีกครั้งจะดีกว่า :) แต่สำหรับกรณีปกติ คำแนะนำของคุณก็เพียงพอแล้ว - person Vlad; 20.10.2011

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

m_C.เริ่มต้น( m_B.getSomeValue() );

person Martin    schedule 20.10.2011

ง่ายที่สุดคือการจัดเก็บพอยน์เตอร์ไปที่ B และ C สิ่งเหล่านี้สามารถเริ่มต้นเป็น 0 โดยไม่ต้องสร้างใดๆ ระวังอย่ายกเลิกการอ้างอิงตัวชี้ว่างและลบออกในตัวทำลายล้างของ A (หรือใช้ std::unique_ptr/boost::scoped_ptr)

แต่ทำไมไม่เริ่มต้น m_B ก่อน (ผ่านการเรียกคอนสตรัคเตอร์ที่เหมาะสม ไม่ใช่ใน A::A() แล้วใช้อินสแตนซ์ B ที่เตรียมใช้งานแล้วนั้นเพื่อเริ่มต้น m_C มันจะเรียกร้องให้มีการเขียนใหม่เล็กน้อย แต่ฉันพนันได้เลยว่ามันจะคุ้มค่ากับการล้างโค้ด

person rubenvb    schedule 20.10.2011

หากคุณไม่ต้องการจัดสรรแบบไดนามิกโดยใช้ new เพื่อเหตุผลด้านความปลอดภัยเกี่ยวกับความยุ่งเหยิง/ข้อยกเว้นของโค้ด คุณสามารถใช้ std::unique_ptr หรือ std::auto_ptr เพื่อแก้ไขปัญหานี้ได้

วิธีแก้ปัญหาที่หลีกเลี่ยง new คือการแก้ไข C เพื่อให้มีกระบวนการเริ่มต้นสองขั้นตอน ตัวสร้างจะสร้างวัตถุ "ซอมบี้" และคุณจะต้องเรียกใช้เมธอด Initialize บนอินสแตนซ์ m_C นั้นเพื่อเริ่มต้นให้เสร็จสิ้น สิ่งนี้คล้ายกับกรณีที่มีอยู่ที่คุณพบซึ่งคุณสามารถส่ง NULL ไปยังตัวสร้างได้ และย้อนกลับเพื่อเริ่มต้นวัตถุในภายหลัง

แก้ไข:

ฉันคิดถึงสิ่งนี้ก่อนหน้านี้ (แม้ว่าจะดูเหมือนวิธีแก้ปัญหาของคนอื่นมากก็ตาม) แต่ฉันต้องได้รับการยืนยันว่าสิ่งนี้จะไม่พังก่อนที่จะเพิ่มโซลูชันนี้ - C++ อาจค่อนข้างยุ่งยากและฉันไม่ได้ใช้มันบ่อยนัก :)

สิ่งนี้สะอาดกว่าคำแนะนำอื่นๆ ของฉัน และไม่ต้องการให้คุณยุ่งกับการใช้งานใดๆ ยกเว้นของ A

เพียงใช้วิธีคงที่เป็นคนกลางในการเริ่มต้นของคุณ:

class A
{
public:
    A();

private:
    static int InitFromB(B& b)
    {
        b.doSomething();
        b.doMore();
        return b.getSomeValue();
    }

    // m_B must be initialized before m_C
    B m_B;
    C m_C;
};

A::A()
    : m_B(1)
    , m_C(InitFromB(m_B))
{
}

โปรดทราบว่านี่หมายความว่าคุณไม่สามารถอนุญาตให้ m_B ขึ้นอยู่กับอินสแตนซ์ของ A หรือ C ได้เลย ในขณะที่วิธีแก้ปัญหาที่ด้านบนของคำตอบนี้อาจอนุญาตให้คุณส่ง A หรือ m_C ไปยังวิธีการของ m_B

person Merlyn Morgan-Graham    schedule 20.10.2011

เพียงใช้นิพจน์ลูกน้ำ:

A::A()
  : m_B(1)
  , m_c(m_B.doSomething(), m_B.doMore(), m_B.getSomeValue())
{
}

ตามที่คนอื่นอธิบายไว้อย่างชัดเจน m_B ควรประกาศก่อนที่ m_C else m_B.doSomething() จะเรียกใช้พฤติกรรมที่ไม่ได้กำหนดไว้

person MSalters    schedule 20.10.2011
comment
สมมติว่า C ไม่มีตัวสร้างพารามิเตอร์ 3 ตัว - person Martin York; 20.10.2011
comment
ในกรณีนี้คุณเพิ่มวงเล็บ: :m_c((foo(), bar(), baz())) - person MSalters; 20.10.2011
comment
ขอบคุณสำหรับการตอบกลับของคุณ แนวคิดนี้ดีและใช้งานได้ดีกับคำแนะนำเล็กๆ น้อยๆ แต่เมื่อพิจารณาถึงจำนวนคำแนะนำที่ฉันต้องทำก่อนที่จะสามารถสร้างอินสแตนซ์ m_C ได้ โจรก็ดูน่าเกลียดสำหรับฉัน - person Atmocreations; 20.10.2011

ที่นี่เรามีแบบเอกสารสำเร็จรูป:

#include <iostream>

class C
{
public:
  C(int i){std::cout << "C::C(" << i << ")" << std::endl;}
};

class B
{
public:
  B(int i){std::cout << "B::B(" << i << ")" << std::endl;}
  void doSomething(){std::cout << "B::doSomething()" << std::endl;}
  void doMore(){std::cout << "B::doMore()" << std::endl;}
  int getSomeValue(){return 42;}
};

หากคุณต้องการสร้างสิ่งก่อสร้างรูปแบบใหม่สำหรับ B ให้พิจารณาสร้างคลาสที่ได้รับ:

class B1 : public B
{
public:
  B1() : B(1)
  {
    doSomething();
    doMore();
  }
};

ตอนนี้ใช้คลาส B1 ที่ได้มาจาก B:

class A
{
private:
  B1 _b;
  C _c;
public:
  A() : _c(_b.getSomeValue()){std::cout << "A::A()" << std::endl;}
};

แล้ว:

int main()
{
  A a;
}

เอาท์พุท:

B::B(1)
B::doSomething()
B::doMore()
C::C(42)
A::A()
person Jossi    schedule 24.06.2014