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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With