c++language-lawyerobject-lifetimector-initializermember-access

Is taking the address of a member of an uninitialized object well defined?


Consider the following example. When bar is constructed, it gives it's base type (foo) constructor the address of my_member.y where my_member is data member that hasn't been initialized yet.

struct foo {
    foo(int * p_x) : x(p_x) {}
    int * x;
};

struct member {
    member(int p_y) : y(p_y) {}
    int y;
};

struct bar : foo
{
    bar() : foo(&my_member.y), my_member(42) {}
    member my_member;
};

#include <iostream>

int main()
{
    bar my_bar;
    std::cout << *my_bar.x;
}

Is this well defined? Is it legal to take the address of an uninitialized object's data member? I've found this question about passing a reference to an uninitialized object but it's not quite the same thing. In this case, I'm using the member access operator . on an uninitialized object.

It's true that the address of an object's data member shouldn't be changed by initialization, but that doesn't necessarily make taking that address well defined. Additionally, the ccpreference.com page on member access operators has this to say :

The first operand of both operators is evaluated even if it is not necessary (e.g. when the second operand names a static member).

I understand it to mean that in the case of &my_member.y my_member would be evaluated, which I believe is fine (like int x; x; seems fine) but I can't find documentation to back that up either.


Solution

  • Surprisingly, using &my_member.y in your code does result in undefined behavior, as specified in the C++ standard. The relevant text can be found in Section 11.9.5 [class.cdtor] paragraph 1, which states:

    For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior. [...]

    To illustrate this point further, consider the accompanying example:

    struct W { int j; };
    struct X : public virtual W { };
    struct Y {
      int* p;
      X x;
      Y() : p(&x.j) {   // undefined, x is not yet constructed
        }
    };
    

    In your code, the member struct has a non-trivial constructor, and y is a non-static member, leading to undefined behavior when its address is taken before initialization. However, taking the address of my_member itself (&my_member) is permissible because it doesn't refer to any non-static member or base class directly.

    Initially, this rule appeared unintuitive to me, assuming that objects inherently contain their non-static members and bases within themselves. However, upon delving into historical documents from WG21 (The ISO C++ committee), I uncovered pertinent information from N0804:

    [...] the rule [...] was adopted because folks wanted to allow base classes and members of non-POD classes to be allocated on the heap.

    Well, it makes some sense (at least).