15

Inspired by my (currently deleted) answer to this question (but there's a summary in my comment thereto), I was wondering whether the constructor for the Derived class in the code below exhibits undefined behaviour.

#include <iostream>

class Base {
public:
    Base(int test) {
        std::cout << "Base constructor, test: " << test << std::endl;
    }
};

class Derived : public Base {
private:
    int variable;
public: Derived() : Base(variable = 50) { // Is this undefined behaviour?
    }
};

int main()
{
    Derived derived;
    return 0;
}

I know that, when the base c'tor is called, the derived object has not yet (formally) been constructed, so the lifetime of the variable member has not yet started. However, this excerpt from the C++ Standard (thanks to NathanOliver for the reference) suggests (maybe) that the object may be used "in limited ways" (bolding mine):

7    Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a glvalue refers to allocated storage ([basic.stc.dynamic.allocation]), and using the properties of the glvalue that do not depend on its value is well-defined. …

Clearly, if variable were a object which itself had a non-trivial constructor, there would (almost certainly) be undefined behaviour here. However, for a primitive (or POD) type like an int, can we assume that the storage for it has been allocated? And, if so, does the last phrase of the above quote hold, or is this still UB?


As an aside, neither clang-cl nor MSVC, even with full warnings enabled, give any diagnostic for the code shown, and it runs as expected. However, I appreciate that neither of those tools qualify as a formal interpretation/enforcement of the C++ Standard.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • 1
    Isn't this specific case moot since the base ctor takes the parameter by value? You might need a reference or pointer parameter to make this meaningful. – Mooing Duck Apr 15 '21 at 16:10
  • I think the lifetime of `variable` starts before the lifetime of the enclosing `Derived` object. – HolyBlackCat Apr 15 '21 at 16:10
  • @MooingDuck But does the `variable` member keep the given value in a well-defined manner? – Adrian Mole Apr 15 '21 at 16:11
  • 2
    @AdrianMole: If I knew the answer to that, I would have posted an answer :P – Mooing Duck Apr 15 '21 at 16:12
  • My thoughts are that `Base(variable = 50)` causes a copy of `variable` to be made, and that copy depends on the value of the glvalue whos lifetime has yet to begin. @HolyBlackCat The lifetime of `variable` should not have started yet per [this](https://timsong-cpp.github.io/cppwp/basic.life#1) – NathanOliver Apr 15 '21 at 16:16
  • 1
    @Jarod42 Here's a [better](https://godbolt.org/z/zsf19nx9c) demo. Only Clang rejects it, GCC & MSVC stay silent. – HolyBlackCat Apr 15 '21 at 16:23
  • @HolyBlackCat: Yeah, I saw my mistake with missing `constexpr Base`. Seems GCC/msvc are buggy here. – Jarod42 Apr 15 '21 at 16:25

1 Answers1

13

The behavior is undefined, regardless of whether or not the Base constructor accepts the parameter by reference.

When control reaches Base(variable = 50), the lifetime of variable hasn't started yet, because data members are initialized after base classes.

So first, writing to it causes UB because the lifetime hasn't started yet. Then, because you pass by value, reading from it is also UB.

[class.base.init]/13

In a non-delegating constructor, initialization proceeds in the following order:

— First, and only for the constructor of the most derived class ..., virtual base classes are initialized ...

— Then, direct base classes are initialized ...

— Then, non-static data members are initialized in the order they were declared in the class definition ...

— Finally, the ... the constructor body is executed.


Idea by @Jarod42: as an experiment, you can try this in a constexpr context, which is supposed to catch UB.

#include <type_traits>
#include <iostream>

struct Base
{
    int x;
    constexpr Base(int x) : x(x) {}
};

struct Derived : Base
{
    int variable;
    constexpr Derived() : Base(variable = 42) {}
};

constexpr Derived derived;

Clang rejects this with:

error: constexpr variable 'derived' must be initialized by a constant expression
note: assignment to object outside its lifetime is not allowed in a constant expression

while GCC and MSVC accept it.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Do the initialization-time of a variable really equal the start of its life-time? If we disregard the assignment in the question, would it be okay to pass a pointer or reference of the variable to the base-class constructor (as long as the base-class constructor doesn't use its indeterminate value)? – Some programmer dude Apr 15 '21 at 16:24
  • 2
    @Someprogrammerdude Per [this](https://timsong-cpp.github.io/cppwp/basic.life#1.2), yes to your first question. – NathanOliver Apr 15 '21 at 16:26
  • 1
    @Someprogrammerdude I think the answer to the second question is also yes, at least per common sense. – HolyBlackCat Apr 15 '21 at 16:29
  • But, can you clarify whether or not, when the base c'tor is called, storage for `Derived` (and, thus, for `variable`) has been allocated? And what are the "limited ways" for which behaviour is well-defined? – Adrian Mole Apr 15 '21 at 16:48
  • @AdrianMole The storage for the entire `Derived` object (including all bases and members) is allocated instantaneously, so yes. – HolyBlackCat Apr 15 '21 at 17:09
  • @HolyBlackCat So, maybe you can see my confusion. What is (or can be) done to an `int` after that allocation that makes the assignment UB? – Adrian Mole Apr 15 '21 at 17:11
  • @AdrianMole You can form pointers/references to it, and nothing more. (If it was a class, in which some members were already constructed, I think you could also call member functions that use those members.) – HolyBlackCat Apr 15 '21 at 17:12