Is it good habit to always initialize objects with {}?
Asked Answered
F

2

3

Initializing objects with new {} syntax like this:

int a { 123 };

has benefit - you would not declare a function instead of creating a variable by mistake. I even heard that it should be habit to always do that. But see what could happen:

// I want to create vector with 5 ones in it:
std::vector<int> vi{ 5, 1 }; // ups we have vector with 5 and 1.

Is this good habit then? Is there a way to avoid such problems?

Fisher answered 30/10, 2015 at 20:57 Comment(5)
Yes because you disable narrowing conversions.Limited
Wait, doesn't this have tons of duplicates?Epistyle
Scott Meyers has an item dedicated to this in Modern Effective C++Tirol
@Columbo: At least some near-dupes anyway. E.g., https://mcmap.net/q/429510/-c-11-universal-initialization-cause-unexpected-initialization/179910, https://mcmap.net/q/429511/-c-11-variable-initialization-and-declaration/179910, for only a couple.Wolver
This question would probably be a better fit for Programmers.SE--and in fact it already has a reasonable dupe there: programmers.stackexchange.com/q/133688/89959Sheepfold
S
4

Frankly, the subtleties of the various initialization techniques make it difficult to say that any one practice is a "good habit."

As mentioned in a comment, Scott Meyers discusses brace-initialization at length in Modern Effective C++. He has made further comments on the matter on his blog, for instance here and here. In that second post, he finally says explicitly that he thinks the morass of C++ initialization vagaries is simply bad language design.

As mentioned in 101010's answer, there are benefits to brace-initialization. The prevention of implicit narrowing is the main benefit, in my opinion. The "most vexing parse" issue is of course a genuine benefit, but it's paltry--it seems to me that in most cases an incorrect int a(); instead of int a; would probably be caught at compile time.

But there are at least two major drawbacks:

  • In C++11 and C++14, auto always deduces std::initializer_list from a brace-initializer. In C++17, if there's only one element in the initialization list, and = is not used, auto deduces the type of that element; the behavior for multiple elements is unchanged (See the second blog post linked above for a clearer explanation, with examples.) (Edit: as pointed out by T.C. in a comment below, my understanding of the C++17 rules for auto with brace initialization is still not quite right.) All of these behaviors are somewhat surprising and (in mine and Scott Meyers' opinions) annoying and perplexing.
  • 101010's third listed benefit, which is that initialization list constructors are preferred to all other constructors, is actually a drawback as well as a benefit. You've already mentioned that the behavior of std::vector<int> vi{ 5, 1 }; is surprising to people familiar with vector's old two-element constructor. Scott Meyers lists some other examples in Effective Modern C++. Personally, I find this even worse than the auto deduction behavior (I generally only use auto with copy initialization, which makes the first issue fairly easy to avoid).

EDIT: It turns out that stupid compiler-implementation decisions can sometimes be another reason to use brace-initialization (though really the #undef approach is probably more correct).

Sheepfold answered 30/10, 2015 at 22:15 Comment(2)
auto i{1,2}; is ill-formed in C++17, and that change is a DR, so you should see it in C++11/14 modes as well.Subinfeudate
@Subinfeudate DR meaning "defect report"? I'm not sure what that means--is it essentially a retroactive change to previous versions of the standard? If so, I didn't realize such a thing existed. Do you happen to know of a good write-up somewhere on the behavior of auto that includes that information? I'd rather link to some other resource than attempt a complete/correct explanation when my main point is just "it's complicated and confusing."Sheepfold
L
4

Initializing objects with list initialization should be preferred wherever applicable, because:

  1. Among other benefits list-initialization limits the allowed implicit narrowing conversions.

    In particular it prohibits:

    • conversion from a floating-point type to an integer type
    • conversion from a long double to double or to float and conversion from double to float, except where the source is a constant expression and overflow does not occur.
    • conversion from an integer type to a floating-point type, except where the source is a constant expression whose value can be stored exactly in the target type.
    • conversion from integer or unscoped enumeration type to integer type that cannot represent all values of the original, except where source is a constant expression whose value can be stored exactly in the target type.
  2. Another benefit is that is immune to most vexing parse.
  3. Also, initialization list constructors are preferred over other available constructors, except for the default.
  4. Also, they're widely available, all STL containers have initialization list constructors.

Now concerning your example, I would say with knowledge comes power. There's a specific constructor for making a vector of 5 ones (i.e., std::vector<int> vi( 5, 1);).

Limited answered 30/10, 2015 at 21:15 Comment(8)
And it doesn't bother you that std::vector<int> vi( 5, 1); looks almost exactly like std::vector<int> vi{ 5, 1}; but does something very different? There are enough pitfalls with brace-initialization that I think it's not helpful to list only its advantages.Sheepfold
@KyleStrand With a knife you can cut bread, you can also cut your fingers in the process, so learn how to properly use a knife. Every feature has its advantages and its disadvantages. IMHO using brace initialization has more advantages than disadvantages.Limited
Clearly. But even if we take your analogy at face-value (and I'm not convinced any modern language should be as "knife-like" as C++ is, let alone as confusing--knives are simple!), in order to use a knife properly without hurting yourself, you must understand the risks as well as the benefits. Your answer explains only the benefits--and there are real risks.Sheepfold
And my comment about the two initializations looking very similar is more than just aesthetics. As Joel Spolsky says, wrong code should look wrong. If two pieces of code look very similar but do very different things, that's somewhat dangerous from a language design (and usage) perspective.Sheepfold
@KyleStrand the looks of something is in most cases subjective. For example is it good to use new when you have smart pointers?Limited
IMHO the best rule of thumb is to never use braces when planning to call an actual constructor, excepting only the default constructor. Braces are good for primitives, default/value initialization, and list initialization. If you're calling a non-default constructor of a non-trivial type, use parentheses. (Edit: my sloppy "actual constructor" means "constructors that don't take an initialization list).Anserine
@101010 By "looks" I mean how the text of the code physically looks. It's not really "subjective" whether those two lines of code look similar; they do look similar. It is subjective whether or not that's a problem. As for new, you need to understand the dangers as well as the benefits in order to use it well--just as with brace initialization. (And no, I personally do not use new where I can avoid it with make_unique.)Sheepfold
By the way, here's an interesting (minor) benefit (in some cases): https://mcmap.net/q/419232/-c-11-member-initialization-list-ambiguitySheepfold
S
4

Frankly, the subtleties of the various initialization techniques make it difficult to say that any one practice is a "good habit."

As mentioned in a comment, Scott Meyers discusses brace-initialization at length in Modern Effective C++. He has made further comments on the matter on his blog, for instance here and here. In that second post, he finally says explicitly that he thinks the morass of C++ initialization vagaries is simply bad language design.

As mentioned in 101010's answer, there are benefits to brace-initialization. The prevention of implicit narrowing is the main benefit, in my opinion. The "most vexing parse" issue is of course a genuine benefit, but it's paltry--it seems to me that in most cases an incorrect int a(); instead of int a; would probably be caught at compile time.

But there are at least two major drawbacks:

  • In C++11 and C++14, auto always deduces std::initializer_list from a brace-initializer. In C++17, if there's only one element in the initialization list, and = is not used, auto deduces the type of that element; the behavior for multiple elements is unchanged (See the second blog post linked above for a clearer explanation, with examples.) (Edit: as pointed out by T.C. in a comment below, my understanding of the C++17 rules for auto with brace initialization is still not quite right.) All of these behaviors are somewhat surprising and (in mine and Scott Meyers' opinions) annoying and perplexing.
  • 101010's third listed benefit, which is that initialization list constructors are preferred to all other constructors, is actually a drawback as well as a benefit. You've already mentioned that the behavior of std::vector<int> vi{ 5, 1 }; is surprising to people familiar with vector's old two-element constructor. Scott Meyers lists some other examples in Effective Modern C++. Personally, I find this even worse than the auto deduction behavior (I generally only use auto with copy initialization, which makes the first issue fairly easy to avoid).

EDIT: It turns out that stupid compiler-implementation decisions can sometimes be another reason to use brace-initialization (though really the #undef approach is probably more correct).

Sheepfold answered 30/10, 2015 at 22:15 Comment(2)
auto i{1,2}; is ill-formed in C++17, and that change is a DR, so you should see it in C++11/14 modes as well.Subinfeudate
@Subinfeudate DR meaning "defect report"? I'm not sure what that means--is it essentially a retroactive change to previous versions of the standard? If so, I didn't realize such a thing existed. Do you happen to know of a good write-up somewhere on the behavior of auto that includes that information? I'd rather link to some other resource than attempt a complete/correct explanation when my main point is just "it's complicated and confusing."Sheepfold

© 2022 - 2024 — McMap. All rights reserved.