186

I understand that C++11's uniform initialization solves some syntactical ambiguity in the language, but in a lot of Bjarne Stroustrup's presentations (particularly those during the GoingNative 2012 talks), his examples primarily use this syntax now whenever he is constructing objects.

Is it recommended now to use uniform initialization in all cases? What should the general approach be for this new feature as far as coding style goes and general usage? What are some reasons to not use it?

Note that in my mind I'm thinking primarily of object construction as my use case, but if there are other scenarios to consider please let me know.

void.pointer
  • 4,983
  • 8
  • 30
  • 40
  • This might be a subject better discussed on Programmers.se. It seems to lean towards the Good Subjective side. – Nicol Bolas Feb 06 '12 at 02:39
  • 7
    @NicolBolas: On the other hand, your excellent answer might be a very good candidate for the c++-faq tag. I don't think we've had an explanation for this posted before. – Matthieu M. Feb 06 '12 at 07:38
  • My opinion: This is less readable than the good old "Type x = y" so do not use it for fundamental types as this brings nothing. Indeed fundamental types [have no constructors](https://stackoverflow.com/questions/5113365/do-built-in-types-have-default-constructors) (so no risk of wrong chosing) *and* as you can (and should) have your compiler warn you when you are doing narrowing conversions without a cast (even MSVC warns against). (I have not enough rep to post an answer). – Gabriel Devillers May 14 '20 at 08:14

3 Answers3

251

Coding style is ultimately subjective, and it is highly unlikely that substantial performance benefits will come from it. But here's what I would say that you gain from liberal use of uniform initialization:

Minimizes Redundant Typenames

Consider the following:

vec3 GetValue()
{
  return vec3(x, y, z);
}

Why do I need to type vec3 twice? Is there a point to that? The compiler knows good and well what the function returns. Why can't I just say, "call the constructor of what I return with these values and return it?" With uniform initialization, I can:

vec3 GetValue()
{
  return {x, y, z};
}

Everything works.

Even better is for function arguments. Consider this:

void DoSomething(const std::string &str);

DoSomething("A string.");

That works without having to type a typename, because std::string knows how to build itself from a const char* implicitly. That's great. But what if that string came from, say RapidXML. Or a Lua string. That is, let's say I actually know the length of the string up front. The std::string constructor that takes a const char* will have to take the length of the string if I just pass a const char*.

There is an overload that takes a length explicitly though. But to use it, I'd have to do this: DoSomething(std::string(strValue, strLen)). Why have the extra typename in there? The compiler knows what the type is. Just like with auto, we can avoid having extra typenames:

DoSomething({strValue, strLen});

It just works. No typenames, no fuss, nothing. The compiler does its job, the code is shorter, and everyone's happy.

Granted, there are arguments to be made that the first version (DoSomething(std::string(strValue, strLen))) is more legible. That is, it's obvious what's going on and who's doing what. That is true, to an extent; understanding the uniform initialization-based code requires looking at the function prototype. This is the same reason why some say you should never pass parameters by non-const reference: so that you can see at the call site if a value is being modified.

But the same could be said for auto; knowing what you get from auto v = GetSomething(); requires looking at the definition of GetSomething. But that hasn't stopped auto from being used with near reckless abandon once you have access to it. Personally, I think it'll be fine once you get used to it. Especially with a good IDE.

Never Get The Most Vexing Parse

Here's some code.

class Bar;

void Func()
{
  int foo(Bar());
}

Pop quiz: what is foo? If you answered "a variable", you're wrong. It's actually the prototype of a function that takes as its parameter a function that returns a Bar, and the foo function's return value is an int.

This is called C++'s "Most Vexing Parse" because it makes absolutely no sense to a human being. But the rules of C++ sadly require this: if it can possibly be interpreted as a function prototype, then it will be. The problem is Bar(); that could be one of two things. It could be a type named Bar, which means that it is creating a temporary. Or it could be a function that takes no parameters and returns a Bar.

Uniform initialization cannot be interpreted as a function prototype:

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{} always creates a temporary. int foo{...} always creates a variable.

There are many cases where you want to use Typename() but simply can't because of C++'s parsing rules. With Typename{}, there is no ambiguity.

Reasons Not To

The only real power you give up is narrowing. You cannot initialize a smaller value with a larger one with uniform initialization.

int val{5.2};

That will not compile. You can do that with old-fashioned initialization, but not uniform initialization.

This was done in part to make initializer lists actually work. Otherwise, there would be a lot of ambiguous cases with regard to the types of initializer lists.

Of course, some might argue that such code deserves to not compile. I personally happen to agree; narrowing is very dangerous and can lead to unpleasant behavior. It's probably best to catch those problems early on at the compiler stage. At the very least, narrowing suggests that someone isn't thinking too hard about the code.

Notice that compilers will generally warn you about this sort of thing if your warning level is high. So really, all this does is make the warning into an enforced error. Some might say that you should be doing that anyway ;)

There is one other reason not to:

std::vector<int> v{100};

What does this do? It could create a vector<int> with one hundred default-constructed items. Or it could create a vector<int> with 1 item who's value is 100. Both are theoretically possible.

In actuality, it does the latter.

Why? Initializer lists use the same syntax as uniform initialization. So there have to be some rules to explain what to do in the case of ambiguity. The rule is pretty simple: if the compiler can use an initializer list constructor with a brace-initialized list, then it will. Since vector<int> has an initializer list constructor that takes initializer_list<int>, and {100} could be a valid initializer_list<int>, it therefore must be.

In order to get the sizing constructor, you must use () instead of {}.

Note that if this were a vector of something that wasn't convertible to an integer, this wouldn't happen. An initializer_list wouldn't fit the initializer list constructor of that vector type, and therefore the compiler would be free to pick from the other constructors.

Nicol Bolas
  • 11,813
  • 4
  • 37
  • 46
  • 15
    +1 Nailed it. I'm deleting my answer since yours addresses all the same points in much more detail. – R. Martinho Fernandes Feb 06 '12 at 03:19
  • 27
    The last point is why I'd really like `std::vector v{100, std::reserve_tag};`. Similarly with `std::resize_tag`. It currently takes two steps to reserve vector space. – Xeo Feb 06 '12 at 03:49
  • are you sure that 5.2 to float is narrowing? See http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html – Johannes Schaub - litb Feb 06 '12 at 06:19
  • @JohannesSchaub-litb: Fixed. – Nicol Bolas Feb 06 '12 at 06:47
  • 7
    @NicolBolas - Two points: I thought the problem with the vexing parse was foo(), not Bar(). In other words, if you did `int foo(10)`, would you not run into the same problem? Secondly, another reason not to use it seems to be more of an issue of over engineering, but what if we construct all of our objects using `{}`, but one day later down the road I add a constructor for initialization lists? Now all of my construction statements turn into initializer list statements. Seems very fragile in terms of refactoring. Any comments on this? – void.pointer Feb 06 '12 at 14:25
  • @RobertDailey: They don't *all* turn into initializer list statements. as I said, the only reason `vector` fails is because the type of the initializer list is `int`, which conflicts with the constructor that takes a single `int`. So it would only be a problem in those specific circumstances. And, as Xeo points out, you can easily use a simple placeholder object as a way of differentiating between those containers. – Nicol Bolas Feb 06 '12 at 16:07
  • 7
    @RobertDailey: "if you did `int foo(10)`, would you not run into the same problem?" No. 10 is an integer literal, and an integer literal can never be a typename. The vexing parse comes from the fact that `Bar()` could be a typename or a temporary value. That's what creates the ambiguity for the compiler. – Nicol Bolas Feb 06 '12 at 16:09
  • Some slight intellectual dishonest in presenting narrowing as a con only to turn it around to another pro ;) but a very good answer. – jk. Dec 18 '12 at 09:47
  • 4
    `std::vector v{100}` will create 100 items when using a standard library that doesn't support initializer lists, which is still a case that portable code needs to deal with. – Daniel James Dec 20 '12 at 19:15
  • 10
    _`unpleasant behavior`_ - there's a new standardese term to remember :> – sehe Dec 26 '12 at 23:07
  • @DanielJames: "*which is still a case that portable code needs to deal with*" Is it? The Microsoft CTP for VC2012 should not be used for production code; Herb Sutter himself said this. They're not even confident enough in it to call it a *beta*. You shouldn't be trying to write "portable code" for something that is that deep in production. – Nicol Bolas Dec 29 '12 at 08:45
  • 2
    @NicolBolas People often use a newer compiler with the operating system's native standard library. They turn on C++11 mode to take advantage of it's features, but don't have library support. The biggest current example is OS X programmers using Clang with GCC 4.2's library in order to target older versions of the operating system (Apple haven't used a more recent version because of licensing issues). Intel's C++11 mode is also a bit troublesome. I don't know anything about Microsoft's CTP, but people will use a beta version. It's inevitable, no matter what Herb Sutter says. – Daniel James Dec 29 '12 at 11:37
  • @DanielJames: A fair point. I was thinking more in terms of the CTP, but by the time it hits Beta, the standard library will be updated. And Microsoft doesn't let you target older versions of their standard library so easily. Also, hasn't Apple switched over to libc++? – Nicol Bolas Dec 29 '12 at 14:41
  • 4
    The "always prefer an initializer list constructor if possible" rule has got to be the new "most vexing parse." Braces can hardly be a "uniform initialization" syntax if there are constructors which they *cannot invoke*, now, can they? – Kyle Strand Feb 18 '15 at 23:51
  • 2
    How do you fix the most vexing parse example if `foo` instead was of some class type that also had an overload for `std::initializer_list`? – void.pointer Feb 19 '16 at 15:35
  • 1
    @void.pointer Empty braces still selects the default constructor in that case. – Kyle Strand Oct 26 '16 at 14:47
  • 1
    I discovered uniform initialisaton for me being a feature that never should have been introduced (sure, personal oppinion, nevertheless...)! Wherever I look, it just makes code less clear than the classic variants (apart from, potentially, the very initial example, but only for those one-line(!) functions). Any other example I opt for the classic variant. About the vector example: Me personally prefers `std::vector v(100);` vs. `std::vector v({100})`. Everything clear, no doubts about what I really intend... – Aconcagua Jan 17 '18 at 11:45
  • "Or it could be a function that takes no parameters and returns a Bar." i think you meant int. if an int variable is being directly initialized by some function, surely the function returns a type int can hold. a Bar() object probably wouldn't work... – Puddle Nov 27 '18 at 09:45
  • I feel like saying "people already have bad habits with auto so who cares if those habits creep into uniform initialization" is terrible advice. It is akin to seeing litter on the ground and saying well its already dirty so just throw your trash on the ground too. You're discounting a legitimate scenario where it's more harm than help. – jterm Apr 22 '19 at 16:35
72

I'm going to disagree with Nicol Bolas' answer's section Minimizes Redundant Typenames. Because code is written once and read multiple times, we should be trying to minimize the amount of time it takes to read and understand code, not the amount of time it takes to write code. Trying to merely minimize typing is trying to optimize the wrong thing.

See the following code:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

Someone reading the code above for the first time won't probably understand the return statement immediately, because by the time he reaches that line, he will have forgotten about the return type. Now, he'll have to scroll back to the function signature or use some IDE feature in order to see the return type and fully understand the return statement.

And here again it's not easy for someone reading the code for the first time to understand what is actually being constructed:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

The code above is going to break when someone decides that DoSomething should also support some other string type, and adds this overload:

void DoSomething(const CoolStringType& str);

If CoolStringType happens to have a constructor which takes a const char* and a size_t (just like std::string does) then the call to DoSomething({strValue, strLen}) will result in an ambiguity error.

My answer to the actual question:
No, Uniform Initialization should not be thought of as a replacement for the old style constructor syntax.

And my reasoning is this:
If two statements don't have the same kind of intention, they shouldn't look the same. There are two kinds of notions of object initialization:
1) Take all these items and pour them into this object I'm initializing.
2) Construct this object using these arguments I provided as a guide.

Examples of the use of notion #1:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Example of the use of notion #2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

I think it's a bad thing that the new standard allows people to initialize Stairs like this:

Stairs s {2, 4, 6};

...because that obfuscates the meaning of the constructor. Initialization like that looks just like notion #1, but it's not. It's not pouring three different values of step heights into the object s, even though it looks like it is. And also, more importantly, if a library implementation of Stairs like above has been published and programmers have been using it, and then if the library implementor later adds an initializer_list constructor to Stairs, then all the code that has been using Stairs with Uniform Initialization Syntax is going to break.

I think that the C++ community should agree to a common convention on how Uniform Initialization is used, i.e. uniformly on all initializations, or, like I strongly suggest, separating these two notions of initialization and thus clarifying the intention of the programmer to the reader of the code.


AFTERTHOUGHT:
Here's yet another reason why you shouldn't think of Uniform Initialization as a replacement for the old syntax, and why you can't use brace notation for all initializations:

Say, your preferred syntax for making a copy is:

T var1;
T var2 (var1);

Now you think you should replace all initializations with the new brace syntax so that you can be (and the code will look) more consistent. But the syntax using braces doesn't work if type T is an aggregate:

T var2 {var1}; // fails if T is std::array for example
TommiT
  • 829
  • 6
  • 4
  • 54
    If you have "" your code will be difficult to understand regardless of syntax. – kevin cline Dec 18 '12 at 17:21
  • 8
    Besides IMO it is the duty of your IDE to inform you what type it is returning (e.g. via hovering). Of course if you don't use an IDE, you took the burden upon yourself :) – abergmeier Dec 22 '12 at 11:44
  • 6
    @TommiT I agree with some parts of what you say. However, in the same spirit as the `auto` vs. _explicit type declaration_ debate, I'd argue for a balance: uniform initializers rock pretty big time in _template meta-programming_ situations where the type is usually pretty obvious anyway. It will avoid repeating your darn complicated `-> decltype(....)` for incantation e.g. for simple oneline function templates (made me weep). – sehe Dec 26 '12 at 23:50
  • 3
    @TommiT I thoroughly agree with your points, there is currently no agreed industry best practice for auto and uniform initialiasation, new language features are generally misused at first and the pain that causes is what informs best practice, Nicol Bolas' answer advocates a far too liberal approach to these new language features in my opinion. Good response highlighting real problems with using his approach. – radman Mar 08 '13 at 23:07
  • 7
    "*But the syntax using braces doesn't work if type T is an aggregate:*" Note that this is a reported defect in the standard, rather than intentional, expected behavior. – Nicol Bolas Mar 10 '13 at 00:40
  • 8
    "Now, he'll have to scroll back to the function signature" if you have to scroll, your function is too big. – Miles Rout Nov 03 '14 at 03:41
  • 1
    I think the last part of your answer starting from "I think it's a bad thing that the new standard allows people to initialize Stairs like this ... because that obfuscates the meaning of the constructor" is much more compelling than the first part of using long functions. – Tim Rae Jul 12 '18 at 01:50
  • just my opinion: completely agree that code should not primarily be easy to write. However, refactorting is imho as important as reading the code. Having less redundant typenames makes refactoring considerably easier (in the extreme you dont need to change anything to make a piece of code work with a different type) – 463035818_is_not_a_number Nov 06 '18 at 10:09
  • When you say "fails if T is std::array", what exactly do you mean by fail there? – Richard Corden Jan 10 '20 at 11:28
-3

If your constructors merely copy their parameters in the respective class variables in exactly the same order in which they are declared inside the class, then using uniform initialization can eventually be faster (but can also be absolutely identical) than calling the constructor.

Obviously, this doesn't change the fact that you must always declare the constructor.

  • 2
    Why do you say it can be faster? – jbcoe Sep 26 '16 at 21:08
  • This is incorrect. There is no requirement to declare a constructor: [`struct X { int i; }; int main() { X x{42}; }`](https://wandbox.org/permlink/pN2E0ZNphQ0FS3IA). It is also incorrect, that uniform initialization can be faster than value initialization. – Tim Jul 19 '19 at 20:42