Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is an object fully constructed at the end of the initialiser list?

This is a spinoff from invoking the copy constructor within the constructor.

I believe that an object is fully formed and can be expected to behave as such by the end of the initialiser list (edit: I was wrong about this though!). Specifically, member functions and accessing local state from within the constructor itself will behave exactly as they would from any other member function.

This seems to be a slightly contentious point of view though, the alternative is that only once the constructor has returned normally is the object fully formed.

The following is a quick & dirty test case for this which shows all the member fields that are mentioned in the initialiser list being initialised and those that aren't getting default constructed.

#include <cstdio>

struct noise
{
  noise() { printf("noise default constructed\n"); }
  noise(int x) { printf("noise integer constructed %u\n", x); }
  ~noise() { printf("noise dtor\n"); }
};

struct invoke : public noise
{
  noise init;
  noise body;
  invoke() : noise(3), init(4)
  {
    body = noise(5);
    throw *this; // try to use the object before returning normally
  }
  ~invoke() { printf("invoke dtor\n"); }
};

int main()
{
  try
    {
      invoke i;
    }
  catch (...)
    {
    }
}

This prints, on my machine at least,

noise integer constructed 3
noise integer constructed 4
noise default constructed
noise integer constructed 5
noise dtor
noise dtor
noise dtor
noise dtor
invoke dtor
noise dtor
noise dtor
noise dtor

As always, it's difficult to distinguish works-as-specified from works-as-my-compiler-implemented! Is this actually UB?

like image 456
Jon Chesterfield Avatar asked Sep 06 '25 03:09

Jon Chesterfield


2 Answers

Is an object fully constructed at the end of the initialiser list?

No it is not. The object this is fully constructed at the end of the execution of the constructor.

However, all the members are constructed by the end of the initializer list.

The difference is subtle but it is important as it relates to the execution of the destructors. Every constructed member and base class is destructed if the this object throws an exception during the execution of the constructor. The destructor of the this object will only execute once it is fully constructed.

From the cppreference:

  • For any object of class or aggregate types if it, or any of its subobjects, is initialized by anything other than the trivial default constructor, lifetime begins when initialization ends.
  • For any object of class types whose destructor is not trivial, lifetime ends when the execution of the destructor begins.
like image 191
Niall Avatar answered Sep 07 '25 22:09

Niall


Your example is well-defined behavior, but only just so.

To be clear, the "invoke dtor" line we're seeing is from the destruction of your exception, not the top-level i object.

Each member of the class is initialized by the time the constructor body starts, though the object itself is not initialized until the constructor body completes. This is why ~invoke is not called on the top-level invoke object.

The throw *this expression copy-initializes an invoke object from *this, which is allowed. (The standard explicitly states that "Member functions [...] can be called during construction or destruction".) Your copy-initialization is the default, which just copy-initializes all the members - which have all been initialized.

Then because your constructor body is exiting via exception, all the initialized members are destructed, the exception is propagated, caught, and then disposed of, calling ~invoke, and in turn destroying those members as well.

like image 25
GManNickG Avatar answered Sep 07 '25 22:09

GManNickG