In the following example, PartInit::x
is left uninitialized after the constructor finishes.
struct AutomaticallyInitialized {
int x;
AutomaticallyInitialized() : x(0) {}
};
struct PartInit {
AutomaticallyInitialized ai;
int x;
int y;
PartInit(int n) :y(n) {
// this->ai is initialized
// this->y is initialized
// Noncompliant: this->x is left uninitialized
}
};
This leads to undefined behavior in benign-looking code, like in the example below. In this particular case, garbage value may be printed, or a
compiler may optimize away the print statement completely.
PartInit pi(1);
std::cout << pi.y; // Undefined behavior
For this reason, constructors should always initialize all data members of a class.
While in some cases, data members are initialized by their default constructor, in others, they are left with garbage.
Types with a default
ed or implicit trivial default constructor follow the aggregate initialization syntax: if you omit them from the
initialization list, they will not be initialized.
struct Trivial {
int x;
int y;
};
struct Container {
Trivial t;
int arr[2];
Container() {
// Noncompliant
// this->t is not initialized
// this->t.x and this->t.y contain garbage
// this->arr contains garbage
}
Container(int) :t{}, arr{} {
// Compliant
// this->t.x and this->t.y are initialized to 0
// this->arr is initialized to {0, 0}
}
Container(int, int) :t{1}, arr{1} {
// Compliant
// this->t.x is 1
// this->t.y is 0
// this->arr is initialized to {1, 0}
}
};
struct DefaultedContainer {
Trivial t;
int arr[2];
DefaultedContainer() = default; // Noncompliant
// this->t and this->arr are not initialized
};
The same is true for a default
ed default constructor.
struct Defaulted {
int x;
Defaulted() = default;
};
struct ContainerDefaulted {
Defaulted d;
ContainerDefaulted() {
// Noncompliant this->d.x is not initialized
}
};
Even if some of the members have class initializers, the other members are still not initialized by default.
struct Partial {
int x;
int y = 1;
int z;
};
struct ContainerPartial {
Partial p;
ContainrePartial() {
// Noncompliant
// this->p.x is not initialized
// this->p.y is initialized to 1
// this->p.z is not initialized
}
ContainrePartial(bool) :p{3} {
// Compliant
// this->p.x is initialized to 3
// this->p.y is initialized to 1
// this->p.z is initialized to 0
}
};
What is the potential impact?
It is a common expectation that an object is in a fully-initialized state after its construction. A partially initialized object breaks this
assumption.
This comes with all the risks associated with uninitialized variables, and these risks propagate to all the classes using the faulty class as a
type as a base class or a data member. This is all the more surprising that most programmers expect a constructor to correctly initialize the members
of its class.
Using garbage values will cause the program to behave nondeterministically at runtime. The program may produce a different output or crash
depending on the run.
In some situations, loading a variable may expose sensitive data, such as a password that was previously stored in the same location, leading to a
vulnerability that uses such a defect as a gadget for extracting information from the instance of the program.
Finally, in C++, outside of a few exceptions related to the uses of unsigned char
or std::byte
, loading data from an
uninitialized variable causes undefined behavior. This means that the compiler is not bound by the language standard anymore, and the program has no
meaning assigned to it. As a consequence, the impact of such a defect is not limited to the use of garbage values.
Exceptions
Aggregate classes do not initialize most of their data members (unless you explicitly value initialize them with x{}
or
x()
) but allow their users to use nice and flexible initialization syntax. This rule ignores them.