Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to prevent omission of aggregate initialization members?

I have a struct with many members of the same type, like this

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

The problem is that if I forget to initialize one of the struct members (e.g. wasactive), like this:

VariablePointers{activePtr, filename}

The compiler will not complain about it, but I will have one object that is partially initialized. How can I prevent this kind of error? I could add a constructor, but it would duplicate the list of variable twice, so I have to type all of this thrice!

Please also add C++11 answers, if there's a solution for C++11 (currently I'm restricted to that version). More recent language standards are welcome too, though!

like image 271
Johannes Schaub - litb Avatar asked Feb 10 '20 12:02

Johannes Schaub - litb


People also ask

What is aggregate initialization in C++?

(since C++20) Otherwise, if the initializer list is non-empty, the explicitly initialized elements of the aggregate are the first n elements of the aggregate, where n is the number of elements in the initializer list. Otherwise, the initializer list must be empty ({}), and there are no explicitly initialized elements.

What is brace initialization?

If a class has non-default constructors, the order in which class members appear in the brace initializer is the order in which the corresponding parameters appear in the constructor, not the order in which the members are declared (as with class_a in the previous example).

What is aggregate type C++?

Formal definition from the C++ standard (C++03 8.5. 1 §1): An aggregate is an array or a class (clause 9) with no user-declared constructors (12.1), no private or protected non-static data members (clause 11), no base classes (clause 10), and no virtual functions (10.3).


Video Answer


5 Answers

Here is a trick which triggers a linker error if a required initializer is missing:

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

Usage:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Outcome:

/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

Caveats:

  • Prior to C++14, this prevents Foo from being an aggregate at all.
  • This technically relies on undefined behaviour (ODR violation), but should work on any sane platform.
like image 64
Quentin Avatar answered Oct 24 '22 17:10

Quentin


For clang and gcc you can compile with -Werror=missing-field-initializers that turns the warning on missing field initializers to an error. godbolt

Edit: For MSVC, there seems to be no warning emitted even at level /Wall, so I don't think it is possible to warn on missing initializers with this compiler. godbolt

like image 25
n314159 Avatar answered Oct 24 '22 19:10

n314159


Not an elegant and handy solution, I suppose... but should works also with C++11 and give a compile-time (not link-time) error.

The idea is to add in your struct an additional member, in the last position, of a type without default initialization (and that cannot initialize with a value of type VariablePtr (or whatever is the type of preceding values)

By example

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

This way you're forced to add all elements in your aggregate initialization list, included the value to explicit initialize the last value (an integer for sentinel, in the example) or you get a "call to deleted constructor of 'bar'" error.

So

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

compile and

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

doesn't.

Unfortunately also

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

doesn't compile.

-- EDIT --

As pointed by MSalters (thanks) there is a defect (another defect) in my original example: a bar value could be initialized with a char value (that is convertible to int), so works the following initialization

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

and this can be highly confusing.

To avoid this problem, I've added the following deleted template constructor

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

so the preceding f4 declaration gives a compilation error because the d value is intercepted by the template constructor that is deleted

like image 21
max66 Avatar answered Oct 24 '22 17:10

max66


For CppCoreCheck there's a rule for checking exactly that, if all members have been initialized and that can be turned from warning into an error - that is usually program-wide of course.

Update:

The rule you want to check is part of typesafety Type.6:

Type.6: Always initialize a member variable: always initialize, possibly using default constructors or default member initializers.

like image 33
darune Avatar answered Oct 24 '22 19:10

darune


The simplest way is not to give the type of the members a no-arg constructor:

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 

Another option: If your members are const & , you have to initialize all of them:

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

If you can live with one dummy const & member, you can combine that with @max66's idea of a 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

From cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

If the number of initializer clauses is less than the number of members or initializer list is completely empty, the remaining members are value-initialized. If a member of a reference type is one of these remaining members, the program is ill-formed.

Another option is to take max66's sentinel idea and add some syntactic sugar for readability

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
like image 32
Gonen I Avatar answered Oct 24 '22 19:10

Gonen I