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.

3
  • This might be a subject better discussed on Programmers.se. It seems to lean towards the Good Subjective side. Feb 6, 2012 at 2: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. Feb 6, 2012 at 7: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 (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). May 14, 2020 at 8:14

3 Answers 3

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.

19
  • 15
    +1 Nailed it. I'm deleting my answer since yours addresses all the same points in much more detail. Feb 6, 2012 at 3:19
  • 27
    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.
    – Xeo
    Feb 6, 2012 at 3:49
  • 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? Feb 6, 2012 at 14:25
  • 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. Feb 6, 2012 at 16:09
  • 10
    unpleasant behavior - there's a new standardese term to remember :>
    – sehe
    Dec 26, 2012 at 23:07
73

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
9
  • 54
    If you have "<lots and lots of code here>" your code will be difficult to understand regardless of syntax. Dec 18, 2012 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, 2012 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, 2012 at 23:50
  • 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. Mar 10, 2013 at 0: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 3, 2014 at 3:41
-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
  • 2
    Why do you say it can be faster?
    – jbcoe
    Sep 26, 2016 at 21:08
  • This is incorrect. There is no requirement to declare a constructor: struct X { int i; }; int main() { X x{42}; }. It is also incorrect, that uniform initialization can be faster than value initialization.
    – Tim
    Jul 19, 2019 at 20:42

Not the answer you're looking for? Browse other questions tagged or ask your own question.