Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can a primitive-type data member of a derived class be used as a parameter to its base constructor?

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.

like image 784
Adrian Mole Avatar asked Apr 15 '21 16:04

Adrian Mole


1 Answers

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.

like image 158
HolyBlackCat Avatar answered Oct 11 '22 16:10

HolyBlackCat