Constructor to specify zero-initialization of all builtin members?
Asked Answered
S

3

5

Is there a simpler way for a class's constructor to specify that all members of built-in type should be zero-initialized?

This code snippet came up in another post:

struct Money
{
    double amountP, amountG, totalChange;
    int twenty, ten, five, one, change;
    int quarter, dime, nickel, penny;

    void foo();
    Money()  {}
};

and it turned out that the problem was that the object was instantiated via Money mc; and the variables were uninitialized.

The recommended solution was to add the following constructor:

Money::Money()
  : amountP(), amountG(), totalChange(),
    twenty(), ten(), five(), one(), change()
    quarter(), dime(), nickel(), penny()
{
}

However, this is ugly and not maintenance-friendly. It would be easy to add another member variable and forget to add it to the long list in the constructor, perhaps causing a hard-to-find bug months down the track when the uninitialized variable suddenly stops getting 0 by chance.

Sanderlin answered 9/4, 2014 at 22:35 Comment(2)
@chris Good idea if there is no user-defined constructor, but could be awkward if the class also has some other code it wants to run on constructionSanderlin
My apologies, I didn't realize how bad that was until a moment ago.Headset
N
7

You can use a subobject to initialize en masse. A member works, but then you need to qualify all access. So inheritance is better:

struct MoneyData
{
    double amountP, amountG, totalChange;
    int twenty, ten, five, one, change;
    int quarter, dime, nickel, penny;
};

struct Money : MoneyData
{
    void foo();
    Money() : MoneyData() {} /* value initialize the base subobject */
};

Demonstration (placement new is used to make sure the memory is non-zero before object creation): http://ideone.com/P1nxN6

Contrast with a slight variation on the code in the question: http://ideone.com/n4lOdj

In both of the above demos, double members are removed to avoid possible invalid/NaN encodings.

Netta answered 9/4, 2014 at 23:8 Comment(27)
Not a bad idea, all things consideredSanderlin
@M.M: Unfortunately, I don't really trust the demonstration, since it prints zero also for the code in the question. g++ must be zeroing during placement new.Netta
Nice, and this avoids problems that a proxy type can run into when being used like the underlying type.Headset
@BenVoigt This solution builds on the fact that C++-specific syntax forces the compiler to initialize members, is this right?Product
@Wolf: I'm not sure I understand your question. Are you asking whether the technique applies to C? There are two things involved here: One, a type with user-defined constructors can never be uninitialized, either the user added a default constructor, or there is no default constructor. Two, when the constructor is invoked during object creation, subobjects get initialized. Here the value-initialization syntax is used for the base subobject. Because the base class is an aggregate, value initialization causes value initialization of each individual member, which sets primitive types to zeroNetta
I see that a struct being initialized in a way, MoneyData() , that cannot be used in C, because initializer lists don't exist there. So the compiler can detect safely, that the programmer is aware the extra-cost... ...I hate these initialization struggles ;)Product
@Wolf: C doesn't have behavior provided by the type at all. You say "initializer lists don't exist there" (and you're correct), but miss the bigger picture. Constructors don't exist there. An object is not in control of its own data in C; initialization is always up to the user.Netta
@BenVoigt So I can be sure that if I embed a C struct in a class with a constructor, that it is initialized? I thought this being only the case if explicitely mentioned in a constructor?Product
@Wolf: With respect to your linked question, I feel your pain because I work in embedded systems where the compilers often are non-conformant (or conform to an old standard). You're on the desktop; I encourage you to switch to a real compiler. I'm pretty sure Borland compilers have always been in the business of creating their own similar language (look at Delphi vs Pascal).Netta
@Wolf: It will be default-initialized if you don't specify otherwise... but that usually means "no initialization". So list it in the initializer-list of the constructor. Or use C++11's new brace-or-equal-initializer syntax, exemplified by the second part of Vlad's answer.Netta
@BenVoigt yes the "no initialization" is my problem, and I mixed something up, here is what I read in your example: ideone.com/6zK1lKProduct
@Wolf: From my answer: "A member works, but then you need to qualify all access. So inheritance is better" Your code is the same but using a member, so yes it works.Netta
@BenVoigt ..., only now I see what you meant by you need to qualify all access sorry for the confusion.Product
A warning: In C++98, if MoneyData also contains a non-POD member (e.g. std::string) then suddenly the int and double members are not zero-initialized any more.Sanderlin
@MattMcNabb: By construction it doesn't contain any non-POD member. Those stay in the main Money object.Netta
For cleanliness, shouldn't MoneyData have a protected non-virtual destructor (stackoverflow.com/a/7404130) ?Jens
@Nemo: Nah, the value of doing that is so low, I'd rather keep the type POD (standard-layout, trivial)Netta
@BenVoight: Your struct Money is already non-POD because it has a non-trivial (user-provided) constructor, and my suggestion preserves the standard-layout-ness.Jens
@Nemo: MoneyData is currently POD, and your suggestion breaks that.Netta
Well, I was assuming MoneyData here was an internal utility class, not intended to be used directly, since the whole point is to implement struct Money. I suppose it depends on what you are trying to do... The dangers of slicing (every time anybody uses this gadget, forever) far outweigh the (one-time) cost of just spelling out the field-by-field initialization, IMO. So I would either just spell it all out as in the question, or I would forbid the slicing with a protected destructor in your version. Matter of taste, I suppose.Jens
@M.M: you says that if MoneyData contains a non-POD member (e.g. std::string) then suddenly the int and double members are not zero-initialized any more in C++98. But this seems to be wrong. See [this] (melpon.org/wandbox/permlink/k1R9skzrkByS5GwK) & [this] (melpon.org/wandbox/permlink/LETztwxNAsLSoZBl) & [this] (cpp.sh/8huay) code tested in clang & g++. It gives 0 both in C++98 & C++03?Herc
@Destructor: You aren't testing with a C++98 compiler, but with a C++17 compiler in C++98 mode. Compiler writers usually use the latest rule in all modes, if possible. And it is possible, because the older Standard didn't specify the outcome, so "zeroed" is just as legal a value for an uninitialized variable as any other.Netta
@BenVoigt: can you tell me the relevant citation from C++98 standard that says that If class contain non pod member like std::string then other data member which are of type like int don't get zero initialized when using Test t=Test(); ?Herc
@Destructor: I only have text for C++03. Sorry. In C++03 the rules make this work even if there is a non-POD class member in MoneyData.Netta
@Destructor: Please read https://mcmap.net/q/17267/-what-do-the-following-phrases-mean-in-c-zero-default-and-value-initialization It was different in C++98. Not because there's any rule that says members don't get zero-initialized, but because the rule that does make them zero-initialized only triggers when the data type is POD.Netta
@Herc In C++98 your code causes undefined behaviour by reading an uninitialized variable, so any output at all would be compliant with the standard. The relevant clauses are 8.5/7 "An object whose initializer is (), shall be default-initialized.", 8.5/5 defines default-initialized "If T is a non-POD class type, the default constructor for T is called ", and 12.1/7 says that implicitly-declared default constructor behaves as if it had an empty function body. 12.6/4 says that nonstatic data members of non-class type, without an initializer, are not initialized by the constructor.Sanderlin
In C++03 it was changed so that () recursively value-initializes an aggregate whether or not it is PODSanderlin
H
7

For something working in C++98, you can use Boost's value_initialized: (live example)

#include <boost/utility/value_init.hpp>
...
struct Money
{
    boost::value_initialized<double> amountP, amountG, totalChange;
    boost::value_initialized<int> twenty, ten, five, one, change;
    boost::value_initialized<int> quarter, dime, nickel, penny;

    void foo();
    Money()  {/*every member is 0*/}
};
Headset answered 9/4, 2014 at 22:57 Comment(2)
Good answer .. am accepting Ben's as it can be done without boost but yours is solid alsoSanderlin
definitely (+) a good use for templates, a trivial implementation is possible without using boost.Product
N
7

You can use a subobject to initialize en masse. A member works, but then you need to qualify all access. So inheritance is better:

struct MoneyData
{
    double amountP, amountG, totalChange;
    int twenty, ten, five, one, change;
    int quarter, dime, nickel, penny;
};

struct Money : MoneyData
{
    void foo();
    Money() : MoneyData() {} /* value initialize the base subobject */
};

Demonstration (placement new is used to make sure the memory is non-zero before object creation): http://ideone.com/P1nxN6

Contrast with a slight variation on the code in the question: http://ideone.com/n4lOdj

In both of the above demos, double members are removed to avoid possible invalid/NaN encodings.

Netta answered 9/4, 2014 at 23:8 Comment(27)
Not a bad idea, all things consideredSanderlin
@M.M: Unfortunately, I don't really trust the demonstration, since it prints zero also for the code in the question. g++ must be zeroing during placement new.Netta
Nice, and this avoids problems that a proxy type can run into when being used like the underlying type.Headset
@BenVoigt This solution builds on the fact that C++-specific syntax forces the compiler to initialize members, is this right?Product
@Wolf: I'm not sure I understand your question. Are you asking whether the technique applies to C? There are two things involved here: One, a type with user-defined constructors can never be uninitialized, either the user added a default constructor, or there is no default constructor. Two, when the constructor is invoked during object creation, subobjects get initialized. Here the value-initialization syntax is used for the base subobject. Because the base class is an aggregate, value initialization causes value initialization of each individual member, which sets primitive types to zeroNetta
I see that a struct being initialized in a way, MoneyData() , that cannot be used in C, because initializer lists don't exist there. So the compiler can detect safely, that the programmer is aware the extra-cost... ...I hate these initialization struggles ;)Product
@Wolf: C doesn't have behavior provided by the type at all. You say "initializer lists don't exist there" (and you're correct), but miss the bigger picture. Constructors don't exist there. An object is not in control of its own data in C; initialization is always up to the user.Netta
@BenVoigt So I can be sure that if I embed a C struct in a class with a constructor, that it is initialized? I thought this being only the case if explicitely mentioned in a constructor?Product
@Wolf: With respect to your linked question, I feel your pain because I work in embedded systems where the compilers often are non-conformant (or conform to an old standard). You're on the desktop; I encourage you to switch to a real compiler. I'm pretty sure Borland compilers have always been in the business of creating their own similar language (look at Delphi vs Pascal).Netta
@Wolf: It will be default-initialized if you don't specify otherwise... but that usually means "no initialization". So list it in the initializer-list of the constructor. Or use C++11's new brace-or-equal-initializer syntax, exemplified by the second part of Vlad's answer.Netta
@BenVoigt yes the "no initialization" is my problem, and I mixed something up, here is what I read in your example: ideone.com/6zK1lKProduct
@Wolf: From my answer: "A member works, but then you need to qualify all access. So inheritance is better" Your code is the same but using a member, so yes it works.Netta
@BenVoigt ..., only now I see what you meant by you need to qualify all access sorry for the confusion.Product
A warning: In C++98, if MoneyData also contains a non-POD member (e.g. std::string) then suddenly the int and double members are not zero-initialized any more.Sanderlin
@MattMcNabb: By construction it doesn't contain any non-POD member. Those stay in the main Money object.Netta
For cleanliness, shouldn't MoneyData have a protected non-virtual destructor (stackoverflow.com/a/7404130) ?Jens
@Nemo: Nah, the value of doing that is so low, I'd rather keep the type POD (standard-layout, trivial)Netta
@BenVoight: Your struct Money is already non-POD because it has a non-trivial (user-provided) constructor, and my suggestion preserves the standard-layout-ness.Jens
@Nemo: MoneyData is currently POD, and your suggestion breaks that.Netta
Well, I was assuming MoneyData here was an internal utility class, not intended to be used directly, since the whole point is to implement struct Money. I suppose it depends on what you are trying to do... The dangers of slicing (every time anybody uses this gadget, forever) far outweigh the (one-time) cost of just spelling out the field-by-field initialization, IMO. So I would either just spell it all out as in the question, or I would forbid the slicing with a protected destructor in your version. Matter of taste, I suppose.Jens
@M.M: you says that if MoneyData contains a non-POD member (e.g. std::string) then suddenly the int and double members are not zero-initialized any more in C++98. But this seems to be wrong. See [this] (melpon.org/wandbox/permlink/k1R9skzrkByS5GwK) & [this] (melpon.org/wandbox/permlink/LETztwxNAsLSoZBl) & [this] (cpp.sh/8huay) code tested in clang & g++. It gives 0 both in C++98 & C++03?Herc
@Destructor: You aren't testing with a C++98 compiler, but with a C++17 compiler in C++98 mode. Compiler writers usually use the latest rule in all modes, if possible. And it is possible, because the older Standard didn't specify the outcome, so "zeroed" is just as legal a value for an uninitialized variable as any other.Netta
@BenVoigt: can you tell me the relevant citation from C++98 standard that says that If class contain non pod member like std::string then other data member which are of type like int don't get zero initialized when using Test t=Test(); ?Herc
@Destructor: I only have text for C++03. Sorry. In C++03 the rules make this work even if there is a non-POD class member in MoneyData.Netta
@Destructor: Please read https://mcmap.net/q/17267/-what-do-the-following-phrases-mean-in-c-zero-default-and-value-initialization It was different in C++98. Not because there's any rule that says members don't get zero-initialized, but because the rule that does make them zero-initialized only triggers when the data type is POD.Netta
@Herc In C++98 your code causes undefined behaviour by reading an uninitialized variable, so any output at all would be compliant with the standard. The relevant clauses are 8.5/7 "An object whose initializer is (), shall be default-initialized.", 8.5/5 defines default-initialized "If T is a non-POD class type, the default constructor for T is called ", and 12.1/7 says that implicitly-declared default constructor behaves as if it had an empty function body. 12.6/4 says that nonstatic data members of non-class type, without an initializer, are not initialized by the constructor.Sanderlin
In C++03 it was changed so that () recursively value-initializes an aggregate whether or not it is PODSanderlin
S
2

You can use the default constructor the following way

Money m = {};

Or you can set initialization of data members inside the class definition:

struct Money
{
    double amountP = 0.0, amountG = 0.0, totalChange = 0.0;
    int twenty = 0, ten = 0, five - 0, one = 0, change = 0;
    int quarter = 0, dime = 0, nickel = 0, penny = 0;

    void foo();
    Money()  {}
};
Sunwise answered 9/4, 2014 at 22:42 Comment(8)
I don't want to rely on the caller doing = {};, I'd rather my object work properly even if they write Money m; m.foo();. The second option is good but only available in C++11, it would be great if there is a C++98 solution also.Sanderlin
Your first solution doesn't work because the OP has a NOP default constructor defined. He needs to either get rid of that, or default it instead of the empty body.Mcminn
@VladfromMoscow, Since there is a user-provided constructor, it is not an aggregate. Thus, the list-initialization value-initializes the object. Since there is a user-provided default constructor, it is default-initialized as a result. Initializers are 8.5, aggregates are 8.5.1 and list-initialization is 8.5.4.Headset
@Headset From the C++ Standard: "— Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized."Sunwise
@VladfromMoscow, Go to 8.5 to the first point of value-initialization. To value-initialize an object of type T means: — if T is a (possibly cv-qualified) class type (Clause 9) with either no default constructor (12.1) or a default constructor that is user-provided or deleted, then the object is default-initialized;Headset
@Headset I thought that using empty braces zero initializes an object with default constructor.Sunwise
@VladfromMoscow, Not as far as I can tell from following along in the standard, my process being what was stated above.Headset
@VladfromMoscow: An object with a user-declared constructor cannot be zero initialized, since that might break invariants that the constructor is supposed to establish. (It might still be pre-initialized to zero if it has static storage duration, but the object's lifetime will not begin until the user constructor has finished execution. There's no way to bypass the user constructor and initialize to zero instead.)Netta

© 2022 - 2024 — McMap. All rights reserved.