Why different behaviour of synthesized default constructor for static and local variable of user defined class type?

Viewed 103

In the sample program below,

Why is the output different for static & automatic variable of user defined class type ?

/* test.cpp */

/* SalesData Class */
class SalesData {

    public:
        SalesData() = default;
        // other member funcations
    private:
        std::string bookNo;
        unsigned int unitsSold;
        double revenue;

};

/*
 * Prints the SalesData object
 */
std::ostream& print(std::ostream &os, const SalesData &item) {
    os << "ISBN :\'" << item.isbn() << "\', Units Sold :" << item.unitsSold
        << ", Revenue :" << item.revenue << ", Avg. Price :"
        << item.avgPrice() << std::endl;

    return os;
}

int main(int argc, char *argv[]) {
    SalesData s;
    static SalesData s2;


    print(cout, s);
    print(cout, s2);

    return 0;
}

Output of the program is this --

$ ./test
ISBN :'', Units Sold :3417894856, Revenue :4.66042e-310, Avg. Price :1.36352e-319
ISBN :'', Units Sold :0, Revenue :0, Avg. Price :0

How is static changing the scene in synthesized default constructor ?

3 Answers

The default constructor has the same behavior on s and s2. The difference is, for static local variables,

Variables declared at block scope with the specifier static or thread_local (since C++11) have static or thread (since C++11) storage duration but are initialized the first time control passes through their declaration (unless their initialization is zero- or constant-initialization, which can be performed before the block is first entered).

and about zero-initialization:

For every named variable with static or thread-local (since C++11) storage duration that is not subject to constant initialization, before any other initialization.

That means s2 will be zero-initialized firstly, as the effect its data members are zero-initialized too; then enterning main() it's default-initialized via the default constructor.

Case I

Lets consider SalesData s; .

In this case an object of type SalesData with name s is constructed using the default constructor of SalesData. Also, note that in this case the variable s is a local nonstatic.

Next since you :

  1. didn't Use in-class initializers to initialize unitsSold and revenue
  2. didn't Use constructor initializer list to initialize unitsSold and revenue

These 2 variables hold indeterminate value. And using these uninitialized variables leads to undefined behavior.

This means when you wrote:

print(cout, s);//this calls print function

And inside the print function's body you have:

os << "ISBN :\'" << item.isbn() << "\', Units Sold :" << item.unitsSold
        << ", Revenue :" << item.revenue << ", Avg. Price :"
        << item.avgPrice() << std::endl; //this is undefined behavior

So in the above statement you're using the uninitialized variables unitsSold and revenue and this results in undefined behavior.

Undefined behavior means anything1 can happen including but not limited to the program giving your expected output. But never rely(or make conclusions based) on the output of a program that has undefined behavior.

This is the reason you saw those numbers(garbage values) like 3417894856 and 4.66042e-310 in this case. So don't rely on the output of your program since it has undefined behavior.

Case II

Now lets consider static SalesData s2;

From Static local variables' initialization documentation:

Variables declared at block scope with the specifier static or thread_local (since C++11) have static or thread (since C++11) storage duration but are initialized the first time control passes through their declaration (unless their initialization is zero- or constant-initialization, which can be performed before the block is first entered).

Now from zero initialization documentation:

Zero initialization is performed in the following situations:

  1. For every named variable with static or thread-local (since C++11) storage duration that is not subject to constant initialization, before any other initialization.

Here comes the most relevant part:

The effects of zero initialization are:

If T is an non-union class type, all base classes and non-static data members are zero-initialized, and all padding is initialized to zero bits. The constructors, if any, are ignored.

Lets apply these above quoted statements to your example static SalesData s2;

In this case the variable s2 is a local static. Here 2 things happen:

  1. the variable s2 is zero initialized. This means all of its non-static data members like unitsSold and revenue are set to 0 according to the above quoted statement. This is the reason the output numbers in this case are all 0 instead of some garbage values.
  2. Next the variable s2 is default initialized using the default constructor.

1For a more technically accurate definition of undefined behavior see this where it is mentioned that: there are no restrictions on the behavior of the program.

Variables with static storage duration are zero-initialised before their dynamic initialisation phase.

unitsSold and revenue are default initialised. That leaves them uninitialised. In the case of static storage, the previous zero initialisation holds. In the case of automatic storage, they have an indeterminate value. Reading the indeterminate value results in undefined behaviour of the program.

Related