I Have No Constructor, and I Must Initialize (Hacker News discussion) goes into the gnarly details of initialization in C++, and all the immense, immense complexity and edge cases around it. As the top-voted comment on HN put it:

After almost 20 years of experience with C++, there are still some gnarly details I wouldn’t have imagined. What a God awful language!

Interestingly, I do not recall ever running into such an issue myself. The strategy I follow and the guidelines I always give:

1. Always, always define your own constructor

As the article says: never leave it to the compiler to work it out. If you just have a plain-old-data style struct, then just put the initializers in-line:

struct Point2D {
    float x = 0.0f;
    float y = 0.0f;
};

This ensures that your values are always something expected, while retaining the ability to use designated initializers:

auto p = Point2D{
   .x = 3.75f,
   .y = 5.0f,
};

The same holds for other special functions, like the copy- and move constructors, as well as copy- and move-assignment operators. If they matter, explicitly declare them, and default them. This avoids other pitfalls, such as a class with a user-defined destructor getting copy constructors but not move constructors generated by the compiler.

2. Always initialize functional-style

Always use the following form for initializing your fields and variables:

auto foo = Foo();
auto bar = Bar(123);
Baz baz = make_baz();

This avoids the differences between T a; and T a{};, makes turns any constructor calls from implicit to explicit, and makes all of your variable initializations look uniform, regardless of whether it happens via a constructor call or a function call. You can see an example above: Baz baz = make_baz();

I also like to do this for copy constructors, for the same reason of explicitness:

auto foo1 = Foo();
auto foo2 = Foo(foo1);

Related: write out the type instead of auto unless it the type is obvious e.g. because it is explicitly specified on the right hand side because it’s a constructor call or a cast – or if the type is really obnoxious to name, such as std::vector<std::unique_ptr<Foo>>::const_iterator. Anybody who has to review your pull requests will thank you, as will anybody with a malfunctioning/crashing clangd. (Yes, I have worked on code bases that killed clangd. Be grateful if you have not.)

A Rust-y aside

One of the things I like about Rust is that it just avoids all of these issues. You always have to explicitly initialize all variables and fields. There are no compiler-generated special member functions. This can make things more verbose sometimes, but removes a huge amount of ambiguity and complexity.