I think the differences here are important, and especially because of the implicit/explicit difference which I believe you are too quick to dismiss.
I'm going to answer a question slightly different than the one you asked: Why use which form? As opposed to: What are the differences?
A x = {<...>};
Prefer this form if it compiles.
It won't compile if the rhs expression has a type that is explicitly convertible to A
. And that is a very good thing! This is a safety feature provided by the language.
Type authors typically (or at least should) reserve implicit construction/conversion for very safe conversions. Such conversions should be lossless and not change the fundamental semantic meaning of the expression between the rhs and the lhs. For example: milliseconds x = 3s;
The rhs is seconds
, and the lhs is milliseconds
. And the construction converts between two time durations, and does not loose any information at all. This is a good place to use this form of construction.
By using this form, the programmer is saying: Just give me the "safe set" of constructors for type A
. If there is a mistake in my expression type on the rhs that might result in an unsafe conversion, I would like to find out about it at compile-time.
When A
is an integral type, then even including the {}
has an advantage:
unsigned x = {i};
This follows the style of saying just give me safe conversions. But it adds an extra belt to your suspenders: Just give me conversions that won't narrow. The {}
is basically an extra stop-gap in latter C++ to make up for design mistakes made in C several decades ago.
However sometimes you need a bigger hammer. Sometimes explicit conversions are what you want and need. This is the time for:
A x{<...>};
For example: milliseconds x{3};
This converts an int
to a millisecond
. Though the conversion is lossless, the type of the lhs is not like the type of the rhs. The rhs could represent 3 anything. 3 Apples. 3 IRS notices. 3 years. This is not a safe conversion to make implicitly. And the std::lib knows this. If you tried, it would not compile. Nevertheless, sometimes this is exactly what you need to do. Reserve this form for that situation.
Failure to follow this advice, could in some situations, lead to run-time errors. Two of which are demonstrated in this lightning talk.
Finally, this is a good form:
auto x = A{<...>};
when you want the type of x
to be A
. And it is especially good when A
is not an easy type to spell, and doesn't even appear on the rhs.
My favorite example of the use of this form is in the std::chrono::round
implementation:
template <class To, class Rep, class Period>
constexpr
To
round(const duration<Rep, Period>& d)
{
auto t0 = floor<To>(d);
auto t1 = t0 + To{1};
if (t1 == To{0} && t0 < To{0})
t1 = -t1;
auto diff0 = d - t0; // here
auto diff1 = t1 - d; // and here
if (diff0 == diff1)
{
if (t0 - duration_cast<To>(t0/2)*2 == To{0})
return t0;
return t1;
}
if (diff0 < diff1)
return t0;
return t1;
}
The lines marked "here and here" result in (in general) a very complicated type for diff0
and diff1
: There's a couple of different ways to spell it. It is the common_type_v<duration<Rep, Period>, To>
. And it really isn't important to the reader of the code exactly what that type is. The only important thing to know is that this type will represent the exact difference in the two operands.
In summary, this is no "best form". They are all good tools to have in your toolbox. And the trick is to know which to use when. And if you get good at it, you will be more skilled than the great majority of C++ programmers.