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.