Code Style & Taste
A blog where I share thoughts on code and practices

The Most Confusing C++ Behavior

What does this program print out? I use placement new here so you could see when members are initializing (or not).
#include <new>
#include <cstdio>
#include <type_traits>
struct A { int a=0; };
struct B { int b{}; };
struct C { int c; C(int c) : c(c) { } };
struct D { int d; D(int d) : d(d) { } D()=default; };
struct E { D e{}; };

int main(int argc, char*argv[])
{
	int data;
	data = -1; auto&a = * new(&data) A;
	printf("A %d %d\n", std::is_trivially_constructible<A>::value, a.a);
	data = -2; auto&b = * new(&data) B;
	printf("B %d %d\n", std::is_trivially_constructible<B>::value, b.b);
	data = -3; auto&c = * new(&data) C{1};
	printf("C %d %d\n", std::is_trivially_constructible<C>::value, c.c);
	data = -4; auto&d = * new(&data) D;
	printf("D %d %d\n", std::is_trivially_constructible<D>::value, d.d);
	data = -5; auto&e = * new(&data) E;
	printf("E %d %d\n", std::is_trivially_constructible<E>::value, e.e);
}
Tap when you're ready. One of these isn't like the others, and usually people didn't mean to do it. What is it?
Tap for the answer and the rest of the section:
Out of all of these, only D is trivial. A trivial constructor doesn't initialize data despite 'constructor' being part of the title.

The output of the above is

A 0 0
B 0 0
C 0 1
D 1 -4
E 0 0

If you want D to initialize with zeros, you need to remember to write `{}`, as E does. The easiest way to not have uninitialized variables is to assign a value inside the class/struct on the lines you declared the variable. You can use is_trivially_constructiblely_copyable, to know if a type can not be memcpy, but you can't really know it's ok to memcpy...

The thing about trivial types is, T* and T& are trivial. You may not want to memcpy (or write to disk/network) structs using those. spans are trivial, but unique_ptr and containers are not. You might not want to bother checking if a type is trivially constructable/copyable unless you're prepared to be a C++ expert, and your codebase is small enough that you're comfortable debugging any bugs caused by pointers and refs getting into places where you didn't mean to.

Now, I'd like you to think about this sample.
struct F { A a; int f; };
long long data2;
data2 = -6; auto&f = * new(&data2) F;
printf("F %d %d %d\n", std::is_trivially_constructible<F>::value, f.a.a, f.f);

F is not trivially constructible, yet F::f isn't initialized. F::a is, but F::f is not. However, if you change the above to `F{};` everything will be initialized. Is everything clear now?

Final thought: For everyone's sanity, have initializing all variables be a rule, so no one breaks it unless they have a really good reason to.