Last time I showed a couple of questions about initialization. Try them here if you haven’t already. In this article, I’ll show you the answers and add more notes about initialization in C++.

About  

I selected the following questions from 25 questions that you can find in my C++ Initialization Story book:


Print version @Amazon
C++ Initialization Story @Leanpub

Moreover, in the book, you can find a few coding exercises to practice skills.

The Quiz  

The quiz consists of 10 questions; below, I grouped them into categories and explained them in one go.

C++ History:  

Which C++ Standard did add in-class default member initializers?

  1. C++98
  2. C++11
  3. C++14
  4. C++17

Answer: C++11 - added as N2756

Will this code work in C++11?

struct User { std::string name = "unknown"; unsigned age { 0 }; };
User u { "John", 101 };
  1. Yes, the code compiles in C++11 mode.
  2. The code compiles starting with C++14 mode.
  3. The code doesn’t compile even in C++20.

Answer: the code compiles starting with C++14 mode; initially, in C++11, aggregate classes weren’t supported. But thanks to N3653, this improved now.

Additionally: in C++20, we got support for initializing bitfields.

inline variables  

Do you need to define a static inline data member in a cpp file?

  1. No, the definition happens at the same place where a static inline member is declared.
  2. Yes, the compiler needs the definition in a cpp file.
  3. Yes, the compiler needs a definition in all translation units that use this variable.

Answer: 1 - with inline variables, there’s no need to separate declaration and definition.

Can a static inline variable be non-constant?

  1. Yes, it’s just a regular variable.
  2. No, inline variables must be constant.

Answer: 1 - it’s a regular variable, so it can be const or not.

See the example:

struct MyClass {
    inline static const int sValue = 777;
};

inline int sGlobal = 100;

int main() { }

Run at Compiler Explorer

NSDMI  

What’s the output of the following code:

struct S {
    int a { 10 };
    int b { 42 };
};
S s { 1 };
std::cout << s.a << ", " << s.b;
  1. 1, 0
  2. 10, 42
  3. 1, 42

Answer: 1 and 42, 1 comes from the initializer S s { 1 }; and then the b member is initialized by NSDMI.

Consider the following code:

struct C {
    C(int x) : a(x) { }
    int a { 10 };
    int b { 42 };
};
C c(0);

Select the true statement:

  1. C::a is initialized twice. The first time, it’s initialized with 10 and then the second time with 0 in the constructor.
  2. C::a is initialized only once with 0 in the constructor.
  3. The code doesn’t compile because the compiler cannot decide how to initialize the C::a member.

Answer: 2 - this time, the code looks strange, as we have a user-defined constructor. Default member initialization happens before the body of the constructor unless the data member is explicitly mentioned in the constructor initialization list. The code is equivalent to the following:

struct C {
    C(int x) : a(x), b (42) { }
    int a;
    int b;
};
C c(0);

But anyway, the member will be only initialized once, not twice, and the code does compile.

Auto  

Assume you have a std::map<string, int> m;. Select the single true statement about the following loop:

for (const pair<string, int>& elem : m)
  1. The loop properly iterates over the map, creating no extra copies.
  2. The loop will create a copy of each element in the map as the type of elem mismatches.
  3. The code won’t compile as a const pair cannot bind to a map.

Answer: 2 - the value type for std::map is std::pair<const Key, T> and not const std::pair<Key, T>. For our case, the code performed extra copies due to the conversion between std::pair<const std::string, int> and const std::pair<std::string, int>& (i.e., const std::string to std::string).

Can you use auto type deduction for non-static data members?

  1. Yes, since C++11
  2. No
  3. Yes, since C++20

Short answer: no… even in C++23 :)

Longer answer:

You can use auto for static variables:

class Type {
    static inline auto theMeaningOfLife = 42; // int deduced
};

However, you cannot use it as a class non-static member:

class Type {
    auto myField { 0 };   // error
    auto param { 10.5f }; // error  
};

The alternative syntax also fails:

class Type {
    auto myField = int { 10 };  
};

Unfortunately, auto is not supported. For example, in GCC, I get:

error: non-static data member declared with placeholder 'auto'

It’s easy for the compiler to deduce the type of a static data member as the initialization happens at the place you declare it. However, it’s impossible for regular data members because the initializer might come from the default member init or the constructor (when you override a default value).

Note: Same happens for CTAD and data members.

Other  

What happens when you throw an exception from a constructor?

  1. The object is considered “created” so it will follow the regular lifetime of an object.
  2. The object is considered “partially created,” and thus, the compiler won’t call its destructor.
  3. The compiler calls std::terminate as you cannot throw exceptions from constructors.

Answer 2:The compiler calls a destructor only for objects that are fully created. Consider a constructor that checks the id parameter and throws an exception:

class Product {
explicit Product(const char* name, unsigned id) : name_(name), id_(id) { 
     std::cout << name << ", id " << id << '\n';
     if (id < 100)
         throw std::runtime_error{"bad id..."};
}
// ...
};

And then:

int main() {
    try {
        Product tvset("TV Set", 123);
        Product car("Mustang", 9);
    }
    catch (const std::exception& ex) {
        std::cout << "exception: " << ex.what() << '\n';
    }
}

Run @Compiler Explorer

When we run the example, we’ll get the output:

TV Set, id 123
Mustang, id 9
TV Set destructor...
exception: bad id...

This time the example creates two objects: TV set and Mustang. In the output, we can notice that both objects call their constructors, but there’s only one destructor invocation (for TV set). Since Mustang threw an exception in the constructor, the destructor won’t be executed.

Since destructors might be called when the compiler performs stack unwinding, they shouldn’t throw exceptions, which might result in calling std::terminate(). Read this C++ Core Guideline suggestion for more information: E.16: Destructors, deallocation, and swap must never fail.

What happens when you compile this code?

struct Point { int x; int y; };
Point pt {.y = 10, .x = 11 };
std::cout << pt.x << ", " << pt.y;

Select the true statement:

  1. The code doesn’t compile. Designators have to be in the same order as the data members in the Point class.
  2. The code compiles and prints 11, 10.
  3. The code compiles and prints 10, 11.

Answer: 1; the initializers have to be in the same order as in the class. See more in my separate blog pos: Designated Initializers in C++20 - C++ Stories

More in the book:  

See more questions and the content in the book: