Có thể ngăn chặn thiếu sót của các thành viên khởi tạo tổng hợp?


43

Tôi có một cấu trúc với nhiều thành viên cùng loại, như thế này

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

Vấn đề là nếu tôi quên khởi tạo một trong các thành viên cấu trúc (ví dụ wasactive), như thế này:

VariablePointers{activePtr, filename}

Trình biên dịch sẽ không phàn nàn về nó, nhưng tôi sẽ có một đối tượng được khởi tạo một phần. Làm thế nào tôi có thể ngăn chặn loại lỗi này? Tôi có thể thêm một hàm tạo, nhưng nó sẽ nhân đôi danh sách biến hai lần, vì vậy tôi phải gõ tất cả ba lần này!

Vui lòng thêm câu trả lời C ++ 11 , nếu có giải pháp cho C ++ 11 (hiện tại tôi bị giới hạn ở phiên bản đó). Nhiều tiêu chuẩn ngôn ngữ gần đây cũng được chào đón!


6
Gõ một nhà xây dựng không có vẻ rất khủng khiếp. Trừ khi bạn có quá nhiều thành viên, trong trường hợp đó, có thể tái cấu trúc theo thứ tự.
Gonen I

1
@Someprogrammerdude Tôi nghĩ rằng anh ta có nghĩa là lỗi là bạn có thể vô tình bỏ qua một giá trị khởi tạo
Gonen I

2
@theWiseBro nếu bạn biết cách mảng / vector giúp bạn nên đăng câu trả lời. Nó không phải là rõ ràng, tôi không nhìn thấy nó
idclev 463035818

2
@Someprogrammerdude Nhưng nó thậm chí là một cảnh báo? Không thể nhìn thấy nó với VS2019.
acraig5075

8
Có một -Wmissing-field-initializerscờ tổng hợp.
Ron

Câu trả lời:


42

Đây là một mẹo gây ra lỗi liên kết nếu thiếu trình khởi tạo bắt buộc:

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

Sử dụng:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Kết quả:

/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

Hãy cẩn thận:

  • Trước C ++ 14, điều này ngăn không cho Footổng hợp.
  • Về mặt kỹ thuật này dựa trên hành vi không xác định (vi phạm ODR), nhưng nên hoạt động trên mọi nền tảng lành mạnh.

Bạn có thể xóa toán tử chuyển đổi và sau đó là lỗi trình biên dịch.
jrok

@jrok có, nhưng nó là một ngay khi Foođược khai báo, ngay cả khi bạn không bao giờ thực sự gọi cho nhà điều hành.
Quentin

2
@jrok Nhưng sau đó nó không biên dịch ngay cả khi việc khởi tạo được cung cấp. godbolt.org/z/yHZNq_ Phụ lục: Đối với MSVC, nó hoạt động như bạn mô tả: godbolt.org/z/uQSvDa Đây có phải là một lỗi không?
n314159

Tất nhiên, ngớ ngẩn với tôi.
jrok

6
Thật không may, thủ thuật này không hoạt động với C ++ 11, vì nó sẽ trở thành không tổng hợp sau đó :( Tôi đã xóa thẻ C ++ 11, vì vậy câu trả lời của bạn cũng khả thi (vui lòng không xóa nó), nhưng một giải pháp C ++ 11 vẫn được ưa thích, nếu có thể.
Johannes Schaub - litb

22

Đối với clang và gcc, bạn có thể biên dịch -Werror=missing-field-initializersđể biến cảnh báo trên các trình khởi tạo trường bị thiếu thành lỗi. đỡ đầu

Chỉnh sửa: Đối với MSVC, dường như không có cảnh báo nào được phát ra ngay cả ở cấp độ /Wall, vì vậy tôi không nghĩ có thể cảnh báo về các trình khởi tạo bị thiếu với trình biên dịch này. đỡ đầu


7

Không phải là một giải pháp thanh lịch và tiện dụng, tôi cho rằng ... nhưng cũng nên hoạt động với C ++ 11 và đưa ra lỗi biên dịch (không phải thời gian liên kết).

Ý tưởng là thêm vào cấu trúc của bạn một thành viên bổ sung, ở vị trí cuối cùng, của một loại không có khởi tạo mặc định (và không thể khởi tạo với giá trị của loại VariablePtr(hoặc bất cứ thứ gì là loại giá trị trước)

Ví dụ như

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

Bằng cách này, bạn buộc phải thêm tất cả các thành phần trong danh sách khởi tạo tổng hợp của mình, bao gồm giá trị để khởi tạo rõ ràng giá trị cuối cùng (một số nguyên cho sentinel, trong ví dụ) hoặc bạn nhận được lỗi "gọi đến hàm tạo đã xóa của 'bar'".

Vì thế

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

biên dịch và

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

không.

Thật không may

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

không biên dịch.

-- BIÊN TẬP --

Như được chỉ ra bởi MSalters (cảm ơn) có một khiếm khuyết (một khiếm khuyết khác) trong ví dụ ban đầu của tôi: một bargiá trị có thể được khởi tạo với một chargiá trị (có thể chuyển đổi thành int), do đó, hoạt động khởi tạo sau

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

và điều này có thể rất khó hiểu.

Để tránh sự cố này, tôi đã thêm hàm tạo mẫu đã xóa sau đây

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

vì vậy f4khai báo trước đưa ra lỗi biên dịch vì dgiá trị bị chặn bởi hàm tạo mẫu bị xóa


Cảm ơn, điều này là tốt đẹp! Nó không hoàn hảo như bạn đã đề cập, và cũng foo f;không thể biên dịch, nhưng có lẽ đó là một tính năng hơn là một lỗ hổng với thủ thuật này. Sẽ chấp nhận nếu không có đề xuất nào tốt hơn điều này.
Julian Schaub - litb

1
Tôi sẽ làm cho hàm tạo thanh chấp nhận một thành viên lớp lồng nhau được gọi là init_list_end để dễ đọc
Gonen I

@GonenI - để dễ đọc, bạn có thể chấp nhận enumvà đặt tên init_list_end(o đơn giản list_end) một giá trị của điều đó enum; nhưng khả năng đọc thêm rất nhiều lỗi đánh máy, vì vậy, với giá trị bổ sung là điểm yếu của câu trả lời này, tôi không biết liệu đó có phải là một ý tưởng hay không.
max66

Có thể thêm một cái gì đó như constexpr static int eol = 0;trong tiêu đề của bar. test{a, b, c, eol}có vẻ khá dễ đọc với tôi
n314159

@ n314159 - cũng ... trở thành bar::eol; nó gần như vượt qua một enumgiá trị; nhưng tôi không nghĩ nó quan trọng: cốt lõi của câu trả lời là "thêm vào cấu trúc của bạn một thành viên bổ sung, ở vị trí cuối cùng, thuộc loại không có khởi tạo mặc định"; các barphần chỉ là một ví dụ nhỏ để thấy rằng giải pháp công trình; "loại không có khởi tạo mặc định" chính xác sẽ phụ thuộc vào hoàn cảnh (IMHO).
max66

4

Đối với CppCoreCheck, có một quy tắc để kiểm tra chính xác điều đó, nếu tất cả các thành viên đã được khởi tạo và điều đó có thể được chuyển từ cảnh báo thành lỗi - tất nhiên thường là toàn chương trình.

Cập nhật:

Quy tắc bạn muốn kiểm tra là một phần của an toàn loại Type.6:

Loại.6: Luôn khởi tạo biến thành viên: luôn khởi tạo, có thể sử dụng các hàm tạo mặc định hoặc khởi tạo thành viên mặc định.


2

Cách đơn giản nhất là không cung cấp loại thành viên không có hàm tạo:

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 

Tùy chọn khác: Nếu các thành viên của bạn là const &, bạn phải khởi tạo tất cả chúng:

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

Nếu bạn có thể sống với một thành viên và thành viên giả, bạn có thể kết hợp điều đó với ý tưởng về một trọng điểm của @ max66.

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

Từ cppreference https://en.cppreference.com/w/cpp/lingu/aggregate_initialization

Nếu số mệnh đề của trình khởi tạo ít hơn số lượng thành viên hoặc danh sách trình khởi tạo hoàn toàn trống, thì các thành viên còn lại được khởi tạo giá trị. Nếu một thành viên của loại tham chiếu là một trong những thành viên còn lại, chương trình không được định dạng.

Một lựa chọn khác là lấy ý tưởng sentinel của max66 và thêm một số đường cú pháp để dễ đọc

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

Thật không may, điều này làm cho Akhông thể di chuyển và thay đổi ngữ nghĩa sao chép ( Akhông phải là tổng hợp của các giá trị nữa, có thể nói) :(
Johannes Schaub - litb

@ JulianSchaub-litb OK. Làm thế nào về ý tưởng này trong câu trả lời chỉnh sửa của tôi?
Gonen I

@ JohannesSchaub-litb: quan trọng không kém, phiên bản đầu tiên thêm một mức độ gián tiếp bằng cách làm cho các thành viên con trỏ. Thậm chí quan trọng hơn, chúng phải là một tham chiếu đến một cái gì đó và các 1,2,3đối tượng là những người địa phương có hiệu quả trong bộ lưu trữ tự động đi ra khỏi phạm vi khi chức năng kết thúc. Và nó tạo ra sizeof (A) 24 thay vì 3 trên một hệ thống có con trỏ 64 bit (như x86-64).
Peter Cordes

Tham chiếu giả tăng kích thước từ 3 đến 16 byte (phần đệm để căn chỉnh của thành viên con trỏ (tham chiếu) + chính con trỏ.) Miễn là bạn không bao giờ sử dụng tham chiếu, có thể ok nếu nó trỏ đến một đối tượng đã hết phạm vi. Tôi chắc chắn lo lắng về việc nó không tối ưu hóa và sao chép nó xung quanh chắc chắn sẽ không. (Một lớp trống có cơ hội tối ưu hóa tốt hơn ngoài kích thước của nó, vì vậy tùy chọn thứ ba ở đây là kém nhất, nhưng nó vẫn tốn không gian trong mọi đối tượng ít nhất là trong một số ABI. Tôi vẫn lo lắng về việc đệm bị tổn thương tối ưu hóa trong một số trường hợp.)
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.