เป็นไปได้หรือไม่ที่จะป้องกันไม่ให้สมาชิกเริ่มต้นรวม?


43

ฉันมีโครงสร้างที่มีสมาชิกประเภทเดียวกันหลายคนเช่นนี้

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

ปัญหาคือถ้าฉันลืมที่จะเริ่มต้นหนึ่งในสมาชิก struct (เช่นwasactive) เช่นนี้

VariablePointers{activePtr, filename}

คอมไพเลอร์จะไม่บ่นเกี่ยวกับมัน แต่ฉันจะมีหนึ่งวัตถุที่เริ่มต้นได้เพียงบางส่วน ฉันจะป้องกันข้อผิดพลาดประเภทนี้ได้อย่างไร ฉันสามารถเพิ่มนวกรรมิกได้ แต่มันจะทำซ้ำรายการตัวแปรสองครั้งดังนั้นฉันต้องพิมพ์ทั้งหมดสามครั้งนี้!

โปรดเพิ่มคำตอบC ++ 11หากมีวิธีแก้ปัญหาสำหรับ C ++ 11 (ขณะนี้ฉัน จำกัด เฉพาะรุ่นนั้น) อีกทั้งมาตรฐานภาษาล่าสุดก็ยินดีต้อนรับเช่นกัน!


6
การพิมพ์ตัวสร้างไม่ได้ฟังดูน่ากลัวนัก เว้นแต่คุณจะมีสมาชิกมากเกินไปซึ่งในกรณีนี้อาจมีการปรับโครงสร้างใหม่ตามลำดับ
Gonen ฉัน

1
@Someprogrammerdude ฉันคิดว่าเขาหมายถึงข้อผิดพลาดคือคุณสามารถละเว้นค่าเริ่มต้นโดยไม่ตั้งใจ
Gonen I

2
@theWiseBro ถ้าคุณรู้ว่า array / vector ช่วยคุณได้อย่างไรควรโพสต์คำตอบ มันไม่ชัดเจนเลยฉันไม่เห็นเลย
idclev 463035818

2
@Someprogrammerdude แต่มันเป็นแม้กระทั่งคำเตือน? ไม่เห็นด้วย VS2019
acraig5075

8
มีการ-Wmissing-field-initializersรวบรวมธง
รอน

คำตอบ:


42

นี่คือเคล็ดลับที่ทำให้เกิดข้อผิดพลาด linker หากไม่มี initializer ที่ต้องการ:

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

การใช้งาน:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

ผล:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

คำเตือน:

  • ก่อนหน้า C ++ 14 สิ่งนี้จะป้องกันไม่Fooให้รวมกันเลย
  • เทคนิคนี้อาศัยพฤติกรรมที่ไม่ได้กำหนด (การละเมิด ODR) แต่ควรทำงานบนแพลตฟอร์มใด ๆ ที่มีสติ

คุณสามารถลบโอเปอเรเตอร์การแปลงจากนั้นเป็นข้อผิดพลาดคอมไพเลอร์
jrok

@ jrok ใช่ แต่มันเป็นหนึ่งในทันทีที่Fooมีการประกาศแม้ว่าคุณจะไม่เคยเรียกผู้ประกอบการจริง
เควนติน

2
@ jrok แต่ก็ไม่ได้รวบรวมแม้ว่าจะมีการเตรียมใช้งาน godbolt.org/z/yHZNq_ ภาคผนวก:สำหรับ MSVC ทำงานตามที่คุณอธิบาย: godbolt.org/z/uQSvDaนี่เป็นข้อบกพร่องหรือไม่
n314159

แน่นอนฉันโง่
jrok

6
น่าเสียดายที่เคล็ดลับนี้ใช้ไม่ได้กับ C ++ 11 เนื่องจากจะกลายเป็นแบบไม่รวมแล้ว :( ฉันลบแท็ก C ++ 11 ออกดังนั้นคำตอบของคุณก็ใช้ได้เช่นกัน (โปรดอย่าลบ) แต่ ยังคงต้องการโซลูชัน C ++ 11 ถ้าเป็นไปได้
Johannes Schaub - litb

22

สำหรับ clang และ gcc คุณสามารถคอมไพล์ด้วย-Werror=missing-field-initializersซึ่งเปลี่ยนคำเตือนเมื่อ initializers ฟิลด์ที่หายไปเป็นข้อผิดพลาด godbolt

แก้ไข:สำหรับ MSVC ดูเหมือนว่าจะไม่มีการเตือนแม้ในระดับ/Wallดังนั้นฉันไม่คิดว่าเป็นไปได้ที่จะเตือนผู้เริ่มต้นหายไปด้วยคอมไพเลอร์นี้ godbolt


7

ไม่ใช่โซลูชันที่สวยงามและสะดวกสบายฉันคิดว่า ... แต่ควรทำงานกับ C ++ 11 และให้ข้อผิดพลาดในการคอมไพล์เวลา (ไม่ใช่การเชื่อมโยงเวลา)

แนวคิดคือการเพิ่มสมาชิกของคุณใน struct ในตำแหน่งสุดท้ายของประเภทที่ไม่มีการกำหนดค่าเริ่มต้น (และที่ไม่สามารถเริ่มต้นด้วยค่าประเภทVariablePtr(หรืออะไรก็ตามที่เป็นประเภทของค่าก่อนหน้า)

ตามตัวอย่าง

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

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

ดังนั้น

foo f1 {'a', 'b', 'c', 1};

รวบรวมและ

foo f2 {'a', 'b'};  // ERROR

ไม่

น่าเสียดายที่ยัง

foo f3 {'a', 'b', 'c'};  // ERROR

ไม่ได้รวบรวม

- แก้ไข -

ตามที่ชี้โดย MSalters (ขอบคุณ) มีข้อบกพร่อง (ข้อบกพร่องอื่น) ในตัวอย่างดั้งเดิมของฉัน: barค่าสามารถเริ่มต้นได้ด้วยcharค่า (ที่สามารถแปลงเป็นint) ได้ดังนั้นเริ่มต้นการทำงานต่อไปนี้

foo f4 {'a', 'b', 'c', 'd'};

และสิ่งนี้อาจสร้างความสับสนอย่างมาก

เพื่อหลีกเลี่ยงปัญหานี้ฉันได้เพิ่มตัวสร้างเทมเพลตที่ถูกลบต่อไปนี้

 template <typename T> 
 bar (T const &) = delete;

ดังนั้นการf4ประกาศก่อนหน้านี้ให้ข้อผิดพลาดในการรวบรวมเพราะdค่าจะถูกดักจับโดยตัวสร้างแม่แบบที่จะถูกลบ


ขอบคุณนี่เป็นสิ่งที่ดี! มันไม่สมบูรณ์แบบตามที่คุณพูดถึงและทำให้foo f;การคอมไพล์ล้มเหลว แต่อาจเป็นคุณลักษณะที่มากกว่าข้อบกพร่องด้วยเคล็ดลับนี้ จะยอมรับถ้าไม่มีข้อเสนอที่ดีกว่านี้
Johannes Schaub - litb

1
ฉันจะทำให้คอนสตรัคบาร์ยอมรับสมาชิกคลาสซ้อนกันแบบ const ที่เรียกชื่ออย่างเช่น init_list_end เพื่อให้อ่านง่าย
Gonen I

@GonenI - เพื่อให้คุณสามารถอ่านได้ enumและชื่อinit_list_end(o เพียงlist_end) ค่าenumนั้น แต่ความสามารถในการอ่านเพิ่มการพิมพ์ดีดจำนวนมากดังนั้นเนื่องจากค่าเพิ่มเติมคือจุดอ่อนของคำตอบนี้ฉันไม่รู้ว่ามันเป็นความคิดที่ดีหรือไม่
max66

อาจจะเพิ่มสิ่งที่ต้องการในส่วนหัวของconstexpr static int eol = 0; ดูเหมือนจะอ่านได้สวยสำหรับฉัน bartest{a, b, c, eol}
n314159

@ n314159 - อืม ... กลายเป็นbar::eol; มันเกือบจะผ่านenumค่า; แต่ฉันไม่คิดว่ามันเป็นสิ่งสำคัญ: หลักของคำตอบคือ "เพิ่มใน struct ของคุณสมาชิกเพิ่มเติมในตำแหน่งสุดท้ายของประเภทโดยไม่ต้องเริ่มต้นเริ่มต้น"; barส่วนหนึ่งเป็นเพียงตัวอย่างเล็ก ๆ น้อย ๆ ที่จะแสดงให้เห็นว่าการแก้ปัญหาการทำงาน; ประเภท "ที่แน่นอนโดยไม่มีการกำหนดค่าเริ่มต้น" ควรขึ้นอยู่กับสถานการณ์ (IMHO)
max66

4

สำหรับCppCoreCheckมีกฎสำหรับการตรวจสอบอย่างชัดเจนว่าหากสมาชิกทั้งหมดได้รับการเริ่มต้นและสามารถเปลี่ยนจากการเตือนเป็นข้อผิดพลาด - ซึ่งโดยปกติจะเป็นหลักสูตรทั่วทั้งโปรแกรม

ปรับปรุง:

กฎที่คุณต้องการตรวจสอบเป็นส่วนหนึ่งของ typesafety Type.6:

Type.6: เริ่มต้นตัวแปรสมาชิกเสมอ: เริ่มต้นเสมออาจเป็นไปได้ว่าจะใช้ตัวสร้างเริ่มต้นหรือสมาชิกเริ่มต้นเริ่มต้น


2

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

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

ตัวเลือกอื่น: หากสมาชิกของคุณเป็นกลุ่ม & คุณต้องเริ่มต้นทั้งหมด:

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

หากคุณสามารถใช้ชีวิตอยู่กับสมาชิกตัวน้อยหนึ่งคนและสมาชิกคุณสามารถรวมเข้ากับแนวคิดของ @ max66 เกี่ยวกับ Sentinel

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

จาก cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

ถ้าจำนวนของ initializer clases น้อยกว่าจำนวนสมาชิกหรือรายการ initializer ว่างเปล่าทั้งหมดสมาชิกที่เหลือจะถูกกำหนดค่าเริ่มต้น ถ้าสมาชิกของประเภทการอ้างอิงเป็นหนึ่งในสมาชิกที่เหลือเหล่านี้โปรแกรมจะไม่ถูกต้อง

อีกทางเลือกหนึ่งคือใช้แนวคิดของ Sentinel ของ Max66 และเพิ่มน้ำตาล syntactic เพื่อให้อ่านง่าย

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK

น่าเสียดายที่สิ่งนี้ทำให้Aไม่สามารถเคลื่อนย้ายได้และเปลี่ยนความหมายของการคัดลอก ( Aไม่ใช่การรวมค่าอีกต่อไปดังนั้นจะพูด) :(
Johannes Schaub - litb

@ JohannesSchaub-litb ตกลง วิธีการเกี่ยวกับความคิดนี้ในคำตอบที่แก้ไขของฉัน?
Gonen ฉัน

@ JohannesSchaub-litb: ที่สำคัญเท่าเทียมกันรุ่นแรกเพิ่มระดับของการอ้อมโดยการทำให้สมาชิกพอยน์เตอร์ ที่สำคัญยิ่งกว่านั้นพวกเขาจะต้องมีการอ้างอิงถึงบางสิ่งบางอย่างและ1,2,3วัตถุที่มีอยู่ในท้องถิ่นได้อย่างมีประสิทธิภาพในการจัดเก็บอัตโนมัติที่ออกไปจากขอบเขตเมื่อฟังก์ชั่นสิ้นสุด และทำให้ sizeof (A) 24 แทน 3 บนระบบที่มีตัวชี้ 64- บิต (เช่น x86-64)
Peter Cordes

การอ้างอิงแบบจำลองจะเพิ่มขนาดจาก 3 ถึง 16 ไบต์ (การเติมเต็มสำหรับการจัดตำแหน่งตัวชี้ (การอ้างอิง) สมาชิก + ตัวชี้) ถ้าคุณไม่เคยใช้การอ้างอิงอาจเป็นไรถ้ามันชี้ไปที่วัตถุที่หายไป ขอบเขต. แน่นอนฉันกังวลว่าจะไม่ปรับให้เหมาะสมและการคัดลอกไปรอบ ๆ จะไม่เกิดขึ้น (คลาสที่ว่างเปล่ามีโอกาสที่ดีกว่าในการปรับขนาดอื่นให้เหมาะสมที่สุดดังนั้นตัวเลือกที่สามที่นี่นั้นแย่ที่สุด แต่ก็ยังมีค่าใช้จ่ายในพื้นที่สำหรับทุกวัตถุอย่างน้อยใน ABIs บางตัวฉันยังต้องกังวลเกี่ยวกับการบาดเจ็บ การเพิ่มประสิทธิภาพในบางกรณี)
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.