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.

3 Answers
3

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.

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.

+1 Nailed it. I'm deleting my answer since yours addresses all the same points in much more detail.
–
R. Martinho FernandesFeb 6 '12 at 3:19

10

The last point is why I'd really like std::vector<int> v{100, std::reserve_tag};. Similarly with std::resize_tag. It currently takes two steps to reserve vector space.
–
XeoFeb 6 '12 at 3:49

2

@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.pointerFeb 6 '12 at 14:25

4

@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 BolasFeb 6 '12 at 16:09

2

std::vector<int> 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 JamesDec 20 '12 at 19:15

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.

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:

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.

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:

If you have "<lots and lots of code here>" your code will be difficult to understand regardless of syntax.
–
kevin clineDec 18 '12 at 17:21

4

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 :)
–
abergmeierDec 22 '12 at 11:44

4

@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).
–
seheDec 26 '12 at 23:50

1

@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.
–
radmanMar 8 '13 at 23:07

3

"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 BolasMar 10 '13 at 0:40

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.