47

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!

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212

5 Answers5

45

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.
Quentin
  • 62,093
  • 7
  • 131
  • 191
  • You can delete the conversion operator and then it's a compiler error. – jrok Feb 10 '20 at 13:12
  • @jrok yes, but it is one as soon as `Foo` is declared, even if you never actually call the operator. – Quentin Feb 10 '20 at 13:19
  • 2
    @jrok But then it does not compile even if the initialization is provided. https://godbolt.org/z/yHZNq_ **Addendum:** For MSVC it works as you described: https://godbolt.org/z/uQSvDa Is this a bug? – n314159 Feb 10 '20 at 13:21
  • Thanks, this is nice! I think writing something invalid into the `operator T` template will move the error from link-time to compile-time? `static_assert(sizeof(T) == 0, "initialization required for this field").` – Johannes Schaub - litb Feb 10 '20 at 13:48
  • 6
    Unfortunately, this trick doesn't work with C++11, since it will become a non-aggregate then :( I removed the C++11 tag, so your answer is viable aswell (please don't delete it), but a C++11 solution is still preferred, if possible. – Johannes Schaub - litb Feb 10 '20 at 13:51
  • @JohannesSchaub-litb my first stab was actually to trigger a compile-time error, but no matter what I did it would fully instantiate everything and error out as soon as `Foo` is declared (see discussion above). My bad for not noticing the C++11 tag. – Quentin Feb 10 '20 at 13:57
  • @Quentin Sorry, I completely misunderstood the C++11 comment! – walnut Feb 10 '20 at 14:43
  • I think linker errors technically cause undefined behavior, don't they? – S.S. Anne Feb 11 '20 at 12:37
  • @S.S.Anne they are a result of ODR violations, which are UB indeed. Pragmatically, a linker which would silently ignore missing symbols would be pretty unusual. – Quentin Feb 11 '20 at 13:13
  • 1
    @Quentin: I'd suggest listing the downside(s) right in the answer, for the benefit of future readers considering using this. It looks like the least intrusive (other than compiler options or static analysis) and might be the least likely to hurt optimization (doesn't change the size at least)! But making a non-aggregate might not be worth it for some use-cases and should be listed in the answer. – Peter Cordes Feb 11 '20 at 15:00
  • This trick is so cool, thank you ;) – Cevik Dec 11 '21 at 13:56
  • Sadly, at least on MSVC, it doesn't seem to work with shared_ptr https://godbolt.org/z/9MqjYfoa6 – Battlechicken Sep 28 '22 at 13:10
  • 1
    @Battlechicken thats... weird. It does work when changing the operator to `operator T&() const` though. I'm not sure why MSVC tries to convert to an intermediate type when a direct conversion exists. – Quentin Sep 29 '22 at 09:12
  • @Quentin indeed, the T& addition seems to work! – Battlechicken Nov 22 '22 at 08:32
23

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

n314159
  • 4,990
  • 1
  • 5
  • 20
6

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

Oleg Om
  • 429
  • 5
  • 15
max66
  • 65,235
  • 10
  • 71
  • 111
  • Thanks, this is nice! It's not perfect as you mentioned, and also makes `foo f;` fail to compile, but maybe that's more of a feature than a flaw with this trick. Will accept if there's no better proposal than this. – Johannes Schaub - litb Feb 10 '20 at 17:00
  • 1
    I would make the bar constructor accept a const nested class member called something like init_list_end for readability – Gonen I Feb 11 '20 at 09:21
  • @GonenI - for readability you can accept an `enum`, and name `init_list_end` (o simply `list_end`) a value of that `enum`; but the readability add a lot of typewriting, so, given that the additional value is the weak point of this answer, I don't know if it's a good idea. – max66 Feb 11 '20 at 11:16
  • Maybe add something like `constexpr static int eol = 0;` in the header of `bar`. `test{a, b, c, eol}` seems pretty readable to me. – n314159 Feb 11 '20 at 12:28
  • @n314159 - well... become `bar::eol`; it's almost as pass an `enum` value; but I don't think it's important: the core of the answer is "add in your struct an additional member, in last position, of a type without default initialization"; the `bar` part is just a trivial example to show that the solution works; the exact "type without default initialization" should depend from circumstances (IMHO). – max66 Feb 11 '20 at 13:46
  • For me, the chief problem with `bar(int)` is that `foo f4('a', 'b', 'c', 'd')` compiles. @max66 suggests an `enum`, I'd suggest an `enum class`. The key is to avoid unintended conversions. – MSalters Feb 11 '20 at 14:11
  • **Note that `bar` does take non-zero space in `foo` in at least some ABIs**, including x86-64 System V. (https://godbolt.org/z/RvwZaK gcc9.2 -O3 -std=gnu++17). If the members are `int`, it takes the size of an `int` (because size must be a multiple of alignment.) So this can easily waste 8 bytes in a struct of 2 or 3 pointers. `foo` is still trivially-copyable, though. But still, the cure seems worse than the disease; especially if you can use a compiler that has warnings for this. Interesting idea, but your answer should catalogue all the downsides. – Peter Cordes Feb 11 '20 at 14:38
  • @MSalters - This is a good point. Added a deleted constructor template to avoid it. Thanks. – max66 Feb 11 '20 at 14:43
  • @PeterCordes - yes: this is a defect (another); but, given the OP requirements, I suspect that there isn't a perfect solution. – max66 Feb 11 '20 at 14:47
  • I don't think so either. (Other than `-Werror=missing-field-initializers`. Part of establishing that is cataloguing the downsides in ideas, so people can consider whether making their program slower is worth it vs. just using a GCC option. I think listing any/all downsides for something that implements a merely "nice to have" feature is totally reasonable and would improve the answer. The fact that the downsides aren't limited to clunky source might totally rule it out for some use-cases. – Peter Cordes Feb 11 '20 at 14:55
  • I just realized that someone can now pass `NULL` by accident, which seems like a realistic risk around `VariablePtr`. I really think the sentinel should have a unique type. – MSalters Feb 11 '20 at 15:08
  • This wouldn't work if you use designated initializers (which is a C++20 addition - though one which has enjoyed support from compilers for quite a while, being a part of the C99 standard), since then you could just initialize the sentinel, forgetting to initialize some member. – Milo Brandt Feb 11 '20 at 18:46
  • You can replace the sentinel's constructors with `explicit bar() {}`, so that it can only be initialized with an explicit `{}`, which should be quick to type and easy to distinguish visually. – ecatmur Apr 09 '20 at 18:08
4

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.

darune
  • 10,480
  • 2
  • 24
  • 62
2

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
Gonen I
  • 5,576
  • 1
  • 29
  • 60
  • Unfortunately, this makes `A` unmovable and changes the copy-semantics (`A` is not an aggregate of values anymore, so to speak) :( – Johannes Schaub - litb Feb 10 '20 at 16:56
  • @JohannesSchaub-litb OK. How about this idea in my edited answer? – Gonen I Feb 10 '20 at 21:06
  • @JohannesSchaub-litb: equally importantly, the first version adds a level of indirection by making the members pointers. Even more importantly, they have to be a reference *to* something, and the `1,2,3` objects are effectively locals in automatic storage that go out of scope when the function ends. And it makes the sizeof(A) 24 instead of 3 on a system with 64-bit pointers (like x86-64). – Peter Cordes Feb 11 '20 at 14:48
  • A dummy reference increases the size from 3 to 16 bytes (padding for alignment of the pointer (reference) member + the pointer itself.) As long as you never use the reference, it's probably ok if it points to an object that's gone out of scope. I'd certainly worry about it not optimizing away, and copying it around certainly won't. (An empty class has a better chance of optimizing away other than its size, so the third option here is the least bad, but it still costs space in every object at least in some ABIs. I'd still also worry about the padding hurting optimization in some cases.) – Peter Cordes Feb 11 '20 at 14:51