The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal Public Domain Dedication

We already have uniform initialization, stop proposing braced initialization as a solution for everything

There are more and more guidelines that I do not like about how someone should initialize variables.

Even the CppCoreGuidelines state:

ES.23: Prefer the {}-initializer syntax

Reason

Prefer {}. The rules for {} initialization are simpler, more general, less ambiguous, and safer than for other forms of initialization.

Use = only when you are sure that there can be no narrowing conversions. For built-in arithmetic types, use = only with auto.

Avoid () initialization, which allows parsing ambiguities.

The listed reasons are only partially true, as there is some missing information. After thinking carefully, I’ve convinced myself that even if it is only a "prefer", it should rather be an "avoid" or a more complex rule.

Which is the reason I’m writing those notes.

Generic code

First of all, it’s important to distinguish between generic code, for example inside a template, or between specific code, where the type and its behavior is more or less known.

Let’s begin with generic code.

Suppose you have a function template <class T, typename…​ A> void foo(A…​ args), and you want to create a local variable v of type T. Which syntax should you prefer? According to the guidelines something like T v{args, …​}, because it is simpler, more general, and less ambiguous.

The preferred form should actually be

auto v = T(args, ...);

because it is more general, not ambiguous at all, easier to read, and even more typesafe.

Why is it so?

The first problem with curly braces is that they hide constructors. If a class has a constructor accepting a std::initialiser_list, like most containers, then the wrong constructor might get called depending on the types used as arguments.

For example std::vector<int> v{55}; initializes std::vector with one element with the value 55, while std::vector<int> v(55); creates a vector with 55 elements.

If, for example, std::make_unique would have been implemented as

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args){
    return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
}

Then it would not have been possible to call std::vector<int>(55) in any way while implementing it as

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args){
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

permits to call both constructors: std::make_unique<std::vector<int>>(55,55); and std::make_unique<std::vector<int>>(std::initializer_list<int>{55,55}), since std::vector<int> v{55,55}; is the nice syntax for std::vector<int> v(std::initializer_list<int>{55,55});. The std::initializer_list is, after all, a single parameter passed to the corresponding constructor.

This reason alone should be sufficient to establish that, at least for generic code, () is a better alternative than {}, because it can be applied to many more use cases.

For those who do not know, using () might lead to a specific parsing problem, also known as most vexing parse.

To avoid this issue, it is possible to use =.

Thus given (example taken from the Wikipedia):

struct Timer {
  Timer() = default;
};

struct TimeKeeper {
  explicit TimeKeeper(const Timer&){}
  int get_time(){return 0;};
};

Instead of writing

int main() {
  TimeKeeper time_keeper(Timer());
  return time_keeper.get_time();
}

which does not compile, we can write

int main() {
  TimeKeeper time_keeper = TimeKeeper(Timer());
  return time_keeper.get_time();
}

or

int main() {
  auto time_keeper = TimeKeeper(Timer());
  return time_keeper.get_time();
}

To avoid accidental conversions, using auto might be a good idea, thus the third form should probably be preferred, as the constructor appears explicitly on the same line, it should be obvious what the type of time_keeper is.

Does, therefore, this form have any drawbacks? As far as I know only one: arrays.

Plain old C arrays can’t be initialized with (), thus int a[4] = ({0,1,2,3}), is invalid (but accepted as an extension in GCC), contrary to int a[4] = {0,1,2,3}. It is also true that arrays, mainly because they decay to pointers, cannot be used easily in generic code.

It is also much easier to specialize a template for arrays instead of trying to specialize a template for all classes that take a std::initializer_list as a constructor parameter, just like std::make_unique does.

Interestingly, () works with aggregates too, as in the following example:

struct aggregate {
  int a;
  int b;
};

aggregate s({1,2});

Of course, it is using internally {} and the generated copy-constructor, without them, it would not work. But since the syntax is valid, using () also works in the most generic context.

As of C++17 aggregate can be initialized with () even with delete copy and move constructors, before C++17, non-moveable and non-copyable types are problematic.

Maybe there should be a proposal to make int a[4] = ({0,1,2,3}) and int a[4]({0,1,2,3}) valid syntaxes too, thus making arrays behave more like aggregates. At least we would have a syntax that would work for all types in the most generic context, without exceptions.

What about the better type-safety and readability? There are some use cases where {} is not a better alternative than (), but those use cases are better shown with specific types.

Specific code

With specific code I mean those cases where the type does not have to be fixed (it might still be defined through a template), but its semantic/behavior is known.

Numbers, characters, and booleans

I bet we all have studied some math at school, and without any doubt int i = 1; feels more natural than int i{1};, as it mimics/reminds the notation i = 1.

The assignment is also easier to type, on most keyboard layouts you need to use a key combination to type { and }, misplaced opening and closing parenthesis are also a common source of typos, and typing on devices like a smartphone is also more difficult. Of course, these are only minor issues when writing code against a compiler since it is his job to verify that every statement is correct.

The major issue is readability. In the first case, it reads "i equal 1", even if you never programmed C++, or programmed in general. How do you read the second example? If you know C++ well enough, it reads "i is value-initialized to 1", which raises the bar for beginners and polyglot programmers for no good reason.

Using the same notation that we usually use in math, makes the syntax easier for everyone, and avoids misunderstandings as {} has other meanings too.

Also if you think about it, one of the (many) selling points of C++ is the ability to overload assignment, operator+, operator-, and so on. This is a design decision, to give end-users the ability to define more expressive interfaces. The canonical example is std::complex or a "biginteger" class, but other examples could be matrixes and vectors (not std::vector), quaternions, and many other numerical entities.

So what’s the point of providing such features to make code more readable if, in the end, we encourage to make everything look equal? Why use operator+ instead of a normal function like int add(int, int); like many other programming languages? And if having operator+ adds some value, why shouldn’t it be the same for initialization?

Yes, it’s true that {} has the advantage that it prevents some narrowing conversion. But it’s not really an advantage…​

unsigned int i{-1}; does not compile, but it makes the code inconsistent with unsigned int i{-1u}, since it is after all the same entity: a number that when is added to 1 has 0 as result. And why should unsigned int i{1}; compile if unsigned int i{-1}; does not?

Thus using {} the compiler not only looks at the type (both 1 and -1 are of type int, and 1u and -1u are of type unsigned int), but also at its value. By doing so, it prevents one of the two equivalent expressions from getting compiled, because both unsigned int i = -1; and unsigned int i = -1u; represent the same logical value. It’s the inverse of unsigned int j = 1u. Should therefore unsigned int j = 1; not compile? Or why not accept int i{"1"} as valid code too?

The unfortunate reason is that in C++ "narrowing", when talking about signed and unsigned conversion, does not have the same meaning in math, which continues to confuse many programmers (myself included).

But this post is about initialization, and talking about numbers is a big topic that should be handled separately.

Let’s acknowledge that {} for some people has an advantage when working with integral types, and let’s look at the main motivation. Most of the time it’s because of the mentality that unsigned means "greater than 0", while actually, it means modulo arithmetic, which is not what we want for most use-cases (containers and constants included). If you realize that unsigned means modulo arithmetic, ie working on (n,+,),n+, and not a subset of (,+,), then unsigned i = -1 might not be perfect, but is completely fine, consistent with math notations and easier to read than something like std::numeric_limits<int>::min(), which hides the important information about the given number.

Even without considering conversions between different integral types, how in the world would be int i{}; more readable than assigning i to a number (0 in this case)? Why would something that looks "empty" initialize to 0?

Again, if you are an "expert" then you know why, but it puts the bar higher for everyone else.

Similar arguments hold for the boolean type. How would bool b{}; be more readable than bool b = false;? And again, assignment for boolean variables is used in other fields too, why introduce a new notation and tell everyone to prefer it for most use cases?

My guideline would therefore be: For numeric types (int, double, unsigned int, float, …​), char, bool, and naked pointers: Do not use any kind of parenthesis, prefer assigning directly to literals (for example 1, -2u, 'b', nullptr, false), or other values. Normally it is much more readable than using {} in any form, especially because using = is an established convention in many fields.

Yes, indeed, it might not be consistent with other types in C++, but why should this be an issue?

This is not about generic code but about specific use cases. Not all types have the same API, should we, therefore, stop writing + and use add because it looks like a function call?

No, it should be as easy as possible to read and write.

We should not be afraid of implicit conversion, there is no need to hope for a warning. GCC, Clang, and MSVC have warnings, just enable the corresponding flag and eventually turn them into errors! Some useful flags for GCC are -Wconversion -Woverflow -Wsign-promo -Wsign-compare, and those for clang are the same plus at least -Wnarrowing. MSVC has similar sets of warnings, and all of them can even be converted to errors.

As always it is easier said than done, especially in a big codebase, and there is also the drawback that those settings, without relying on compiler-specific #pragma, are global. It is "easy" to exclude code that we do not own (use -isystem in GCC and Clang to mark "system" headers, the MSVC compiler offers 🗄️ /external:), but inside our code-basis, we need to rely on the build system, for applying different flags, and there might be still some edge cases that are not so easy to solve, without locally disabling the warnings.

There is also another downside: the information with which flags the code is compiled, is not available in the source code itself (unless using a #pragma), so generally, it is better to compile everything with the same flags and make all conversions between different integral types explicit.

Enumerations

The same guidelines given for integral boolean and character types hold for enum and especially for enum class. But I wanted to mention them separately because using {} with enumerators has a different behavior.

The selling point of enum class is being a lot more type-safe than enum.

Given enum class ec {a=10, b=20}; it should not be possible to create an instance of ec with a value that is not ec::a or ec::b unless explicit casting (and possibly invoking undefined behavior).

This seems great, code like ec v = 1; does not compile, contrary to ec v = ec::a.

Unfortunately ec v{}; compiles, and what value does v hold? ec::a or ec::b? It holds static_cast<ec>(0), even if it does not coincide with any of the declared enumerations.

I believe this is a bug/misfeature in the language because using {} with an enum class bypasses the selling point of using enum class.

But it gets worse; ec v{0} compiles too since C++17, while ec v(0) does not, with the error message (from GCC) "cannot initialize a variable of type 'ec' with an rvalue of type 'int'".

So much for the supposed safety provided by {} and enum class.

There are surely use cases where it would be nice to initialize an enum with any value of the underlying type (a custom integer type comes to mind) but would have it been better to assign such a feature to a new type?

Or why not define a structure with just one member variable? It should have no padding, and thus have the same size as the contained type.

Note 📝
MSVC, Clang, and GCC already do this, even with optimizations disabled, this behavior is not standardized, but I know no reason why it could not be.

Now, enum class has, together with {}, some of the disadvantages of a normal enum, that both enum class and {} wanted to solve.

If you are mainly using enum class for enumerating, then using mainly {} instead of () might lead to subtle bugs.

So, it’s just the same as with plain old integers. Avoid if possible any type of parenthesis, use literals, and assignment. The most straightforward way is the one that has minor possibilities to create bugs.

At this point, I’ve nearly shown all primitive types. Only arrays and strings are left, but I’ll discuss them together with other containers.

Collections of objects (containers, aggregates, …​)

Classes and structures that hold values, without enforcing any invariant on the contained item should be initialized with {}, if what we want is initializing the contained item with such values.

Examples of such classes are: arrays, std::array, std::vector, std::tuple, std::pair, smart (and raw) pointers, std::variant, std::optional, std::map, all other containers, and therefore also classes like struct handle{int value;};, and many others.

Let’s use std::vector as an example:

std::vector<int> v{55}; initializes the contained value, while std::vector<int> v(55); performs some logic, and does not initialize the contained value with the given parameter.

The same holds for struct handle{int v;};, it can and probably should be initialized as handle v {value};

std::array is known to be an exception for the number of parentheses, the canonical example where the inconsistency arises is std::array<std::complex<double>> or std::array<std::pair<int, int>>.

std::array<std::pair<int,int>,2> a{{1,1},{2,2}}; does not compile, the correct form is std::array<std::pair<int,int>,2> a{{{1,1},{2,2}}};, or using T = std::pair<int,int>; std::array<T,2> a{ T{1,1}, T{2,2}};.

The same holds for std::complex. std::array<std::complex<double>,3> v{{1}, {1,2}, {54}}; yields a compiler error, and the correct form would be std::array<std::complex<double>,3> v{{{1}, {1,2}, {54}}};

But let’s take advantage of the fact that C++ gives us better tools to match the domain logic. Isn’t std::array<std::complex<double>,3> v{1, 1.+2i, 54}; easier to read and write, even if I needed to write 1. or 1.0 instead of 1?

Using {} for containers and aggregates has another advantage, that has nothing to do with C++. It is the same syntax used for describing sets!

It maps so well to the C++ convention for containers, that I cannot believe that it happened by accident. By using {} it seems natural that all expressions using it should be compiled to some sort of collection, in C++ it’s std::initializer_list, or the collection it’s explicitly assigned to.

Thus this also lowers the bar for novice programmers, as they might have seen a similar syntax somewhere else.

Notice that always using {} for containers has a drawback, consider:

std::vector<int> a = {1,2,3,4,5};
auto s0 = std::vector{a.begin(), a.end()}; // 2 iterator elements, vector of iterators
auto s1 = std::vector(a.begin(), a.end()); // 5 int elements, vector of int

in this case, it would be unusual for someone to want a set with a couple of iterators, but not impossible.

This is why my rule is not to use {} for containers, but to use it if the container is going to be initialized with the contained content. In this case, the usage of {} maps well with the mathematical notation.

Strings

What about std::string (and std::string_view and other string classes)? It’s a container, so using {} seems to be the right choice.

But strings are a little bit special. Literals and std::string have one structural invariant: the trailing '\0'. std::string_view does not have such invariant, and other custom string classes might not have it too, but still, they all have something else in common.

Strings literals are already delimited, not by parentheses, but by ", so adding another delimiter is redundant. Considering that nearly all languages use quotation marks (single or double), suggesting to use of braced parenthesis as an additional delimiter does not make the code necessarily easier to understand.

Also for the empty std::string, using {} seems strange, especially since the string will contain at least the \0 terminator, so it’s not a (mathematical) empty set, or empty container (even if the trailing '\0' is not counted in .size()).

If we want to be very explicit that the string is initialized empty, and not by accident, we can write std::string s = "";, as an alternate form to std::string s;. This would normally have a runtime cost (it calls std::strlen internally), and increase the binary size (because of the global empty string literal).

C++ is a compiled language, there are of course no guarantees, but MSVC, GCC, and clang all optimize such drawbacks completely away with -O1 or better, and generate for both std::string s = "";, std::string s; and std::string s{}; the same assembly.

Therefore performance and binary size should not be an argument for a less readable statement. (The same holds of course for other containers and types too). I’m not stating that debug builds should not be fast too, especially when fuzzing or running under Valgrind performance is just as important. Since there is no allocation, the runtime cost even in those scenarios is negligible.

Also, the fact that std::string s1 = {}; (or std::string s1{};) and std::string s2 = {""}; (or std::string s2{""};) generates two objects with the same value, while with other containers (and auto) passing zero or more arguments, creates objects with different sizes, is another inconsistency to take into account.

Similarly to numbers, my advice is to avoid braces and prefer literals as long as possible, and similar to other containers prefer to use = when assigning values.

auto and {}

Normally we use parenthesis for grouping values. It is a convention used in math, physics, many other programming languages, and other disciplines.

Parameters for a function are, for example, grouped with (), and expressions, for changing the evaluation order, are grouped with () too.

For grouping multiple values in a set (mathematical sense) or container (C++ sense, array included), we use {}.

It seems a simple concept, but the language is not that consistent:

auto v1{};
auto v2{1};
auto v3{1,2};
auto v4 = {};
auto v5 = {1};
auto v6 = {1,2};
  • v1 does not compile, which is fine since it would be a collection containing what type of elements? We could have defined something similar std::nullptr_t for empty sets, but the current behavior is better than many other possible outcomes.

  • v2 compiles to int. This is unfortunate since compiling to a collection of one int (why would you group it with some parenthesis otherwise?) seems a more natural choice, while, as mentioned before, initializing an int with {} is not.

  • v3 does not compile.

So using {} without = either does not compile, or behaves as if it would not be a collection of elements, but the single element contained.

Since both int i{} and int i{0} compile to the same, it also seems inconsistent that auto v{} and auto v{0} do not compile to the same, but I’m not going to argue that we want this behavior.

  • v4 does not compile, but as for v1, it is probably better so.

  • v5 finally compiles, and creates a collection: std::initialiser_list<int>

  • v6 compiles again to std::initialiser_list<int>

It’s clear that the committee had something different in mind with {}, even if it plays so well (except as shown with auto) with the mathematical syntax. This example with auto shows again that using {} for everything is not that easy, and that it has inconsistencies.

With the mathematical notation in mind and all other containers, the general advice would be to prefer {} with = instead of without, but there are use cases where we cannot use = that I’ll show later.

So I’m not concluding much here, just that the advice "prefer {}" seems misguided, and that we should prefer = { /* elements */ } to express better the intent when initializing collections of objects because it is always consistent and probably better to read.

As a (not so) fun fact: when C++11 was standardized, all variables, from v2 to v6 with v4 excluded defined a std::initialiser_list<int>, which is what I would have expected, and find more consistent with the rest of the language.

Unfortunately, someone noticed it and both n3922 🗄️ and n3681 🗄️ were discussed and accepted. This is how we ended (IMHO) with a much bigger inconsistency.

The background seems to be, that many people (possibly not all, otherwise there would not have been inconsistencies with auto) want to teach that every variable could and should be initialized with {}.

I hope that in the future this idea will change, even if it is too late to change again the behavior of auto v2{1};.

Just for the sake of completeness, let’s look at how the code behaves if we would have used ()

auto v1();
auto v2(1);
auto v3(1,2);
auto v4 = ();
auto v5 = (1);
auto v6 = (1,2);
  • v1 compiles to a function declaration that takes no values. This is, unfortunately, the big inconsistency when using (), but it can be avoided by using = together with (), no need for using different symbols.

  • v2 and v5 compile both to an int.

  • v3 and v4 do not compile.

  • v6 compiles to an int, as it uses the comma operator internally, and is thus equivalent to v5. This is inconsistent with v3 and might cause surprises.

One last note auto v5 = ( (1) ) still compiles to an int, while auto v5 = {{1}} does not. Thus parents are idempotent in this (and other) contexts, while I’m not aware of any situation where braces are.

Other objects with invariants

At this point, I’ve mentioned fundamental types, collections of objects, and strings.

What about the rest of the world? All those custom classes with a not-so-generic interface or well-defined behavior?

Objects that have some invariants, should get initialized most straightforwardly, just as we use = for initializing a number.

If there are some invariants, it might be better to use () to indicate that the passed value is not necessarily stored (or accessible) in the initialized object. () are also used, when defining and implementing constructors, just like all other functions, so why use two different syntaxes if there is no good reason?

Consider auto w = Window{10} and auto w = Window(10).

In the first form, I (and I know this is a personal opinion) would assume to be able to get the value 10 out of w, just the way I can with any container, aggregate, integral type, or smart pointer. This thinking is also encouraged by the fact that when using {} on fundamental types, there is no narrowing conversion, so the value does not get reinterpreted in some other way. And here lies the problem.

For specific use cases, there is little reason for trying to initialize everything the same way. It hurts readability and expectations.

In the case of Window, we are not creating a window of 10, but we are converting 10 into a window object, or using 10 for creating a window instance.

The Window constructor might use the value 10 for logging, as a coordinate, title, internal id, or something else. In those cases, why should the code look as if I could get a 10 out of a Window?

Better make it clear that we are using a constructor, that uses the input parameter for doing something.

Making it explicit does not harm, we could also make it more explicit by casting, for example, auto w =static_cast<Window>(10), but you have to draw somewhere the line, and the static_cast is just noise. Calling the constructor is or at least should be, the line, for the same reason that we do not use an alternate syntax for calling functions.

In case something like Window w = 10 compiles, then consider fixing the Window class (make the constructo explicit), unless it represents some numerical quantity (in that case, consider changing the name of the class). If some type is broken or converts implicitly for no good reason, there is little defense against it, using {} for initialization adds very little protection, and since it has drawbacks, I cannot recommend it as a general rule.

Fixing the offending class would fix the problem for every user, instead of teaching everyone to use one syntax instead of another because of subtle differences.

Lambdas

This is another interesting object type that I did not consider before.

Instead of writing auto l = [](){return 1;};, since we should we should prefer using {} for initializing, we should write auto l = {[](){return 1;}};. This agglomerate of parenthesis does compile and creates a std::initializer_list of one lambda. auto l {[](){return 1;}}; compiles to the wanted lambda, but I doubt someone could argue that it is more readable than an assignment.

In case you were asking yourself, yes, auto l ([](){return 1;}); and auto l = ([](){return 1;}); compiles too, just arrays do not work with (). I do not believe that adding more parentheses (lambdas have already enough) just for consistency as a general rule does make the code more readable or easy to teach because it would be more consistent. It does not make code simpler, and this is a good recipe for hiding bugs.

Same rules as if it were integers: just use assignment. Together with auto, there are no unintended conversions.

What to do when it is not possible to use =?

Generally speaking, using = makes it clear that there is an assignment or initialization. The same notation is used in other fields, so this is a big advantage.

Most examples describing these guidelines (granted, mine too) are one-liner, just initializing one or two variables, so using or not using = does not make a big difference in readability if you know C++.

But inside a bigger piece of code, between statements, the = symbol (maybe surrounded by spaces), is much easier to recognize than { and } which, depending on your fonts, might even look similar to ( and ), and it’s easy to overlook them or confuse, for example, with function calls.

So most of the time int a = function(); instead of int a{function()}; (and int a(function());) is easier to read and acknowledge that a new variable has been declared and initialized. And if the variable was already declared, just remove the type: a = function();.

Assignment and initialization should go hand in hand, therefore it’s fine if both of those syntaxes are nearly equal. If a class is doing something else, consider fixing it, or there must be a very good reason to behave differently.

Note that adding a space before or after { does not change much in terms of readability, at least in my experience.

There are situations where we cannot write =, but most guidelines do not say anything explicitly about those cases.

The first example that comes to mind is when returning a variable.

I would not recommend writing return {1}; instead of return 1;, it is a pessimization for more complex types (looking at RVO) but I’ve heard people arguing for it as more consistent with "always prefer {} ".

Another use-case are function parameters. Suppose there is a function void enable(bool). The expression enable({}) is probably the less readable, enable({false}) is misleading (are we passing false or something that takes false as a constructor?), and enable(false) is probably the easiest to write, read and understand, unless there is an implicit conversion, but {} does not help to prevent it.

And last, but not least, another place where we cannot use = is during the initialization of member variables in a constructor. In this case, we need to choose between {} and ().

Deciding which form is more readable between foo(var a_): a{a_}{} and foo(var a_): a(a_){} is not always straightforward.

My guideline is just to apply the same rule between {} and (), but without taking = into consideration: If a is some sort of collection and a_ has the type of the contained element, then use {} (otherwise it would not work for arrays, and since the type is the same, it does not cause problems with enum class). If a_ and a are of the same type, using {} might prevent some error (unintended conversions) if during refactoring the type of a or a_ changes, but for enum class it’s the opposite. Since I believe that in the majority of cases, member variables of classes are not enums, probably it is safe to use {}. In all other cases, prefer to be explicit that we are doing some conversion through the constructor and use ().

Notice that in this case, we do not have edge cases like auto v{} or auto v{1}, because the type is never deduced, it has been declared somewhere else.

Design decisions for custom types

The main selling point of using {} without = is consistency, and to avoid unintended implicit conversion.

I’ve found cases where {} is less consistent than (), (integral conversions, auto) where it cannot be used (classes with std::initializer_list) and where I believe the conversion should be a compiler error (enum class since C++17).

The solution to the "unintended implicit conversion" does not lie in finding a "universal" syntax. It will always be possible to break it.

The solution is designing your types with constructors and as few implicit conversions as possible.

For example, if there is a class that should be initialized with an int, but not signed int, delete the constructor taking an unsigned int to prevent signed/unsigned conversion, without depending on your client using {} instead of () (or fix the implicit signed-unsigned conversions, which will never happen, as it will break too much code).

This has also the advantage that the conversion between unsigned types and your class is prohibited based on the type you are passing to the constructor, not the literal value. Another advantage is that the end-user of the class does not need to use a less familiar syntax or know the difference between apparently similar syntaxes for initializing a variable.

struct conv{
    explicit conv(unsigned int){}
    explicit conv(int) = delete;
};

conv c(1); // does not compile, good
conv c(1u); // compiles
conv c(1ul); // does not compile, good

{} would only solve partially the problem

struct conv{
    explicit conv(unsigned int){}
};

conv c{1}; // compiles, bad. 1 does not have modulo behaviors (behaves like Z), and the constructor requires a type with modulo semantic
conv c{1u}; // compiles
conv c{1ul}; // compiles, bad. `1ul` has modulo semantic, but on a different value, thus the conversion is undesired, as it changes the meaning of the value

Using a straightforward initialization and in general syntax also reduces error possibilities at write time. Of course, you might need to maintain some old classes that implicitly convert, in the case of integral types, all compilers have warnings for implicit and possibly unintended conversions.

static_assert

Other tools, this time as part of the language, for avoiding unintended conversions are auto, decltype(auto), static_assert, and std::is_same. Their usage is not always straightforward and sometimes too verbose, depending on how much you want to check, but it is often possible to hide those details behind a macro or function. A macro has the benefit of making it possible to provide a better error message in case of failure.

template <class T, class U>
U&& assert_type(U&& u){
    static_assert(std::is_same<U, T>::value, "type mismatch")
    return u;
}
template <class T, class U>
void assert_type_v(U&& u){ // array support
    static_assert(std::is_same<U, T>::value)
}

// ugh, macro, but at least it has a better error message
#define ASSERT_TYPE(var, type) \
    static_assert(std::is_same<decltype(var), type>::value, #var " is not of type " #type)


// even if both those lines compile, it still does not mean that `foo` returns an `int`
int v = foo(args);
auto v = foo(args);

// both those checks fail if foo does return something convertible to int, but not int itself
ASSERT_TYPE(v,  int);
assert_type<int>(v);

// or even
int v = assert_type<int>(foo(args));

The disadvantage is that compared to {}, even by using a macro or function template, it is much more verbose. At least it can also be reused in other situations.

Conclusion - summary guideline on how to initialize variables

So we might not have the ultimate tool in the language for avoiding this type of error, but we should take into account who is reading our code. For the programmers, it should be simple to read and understand. For the compiler using {} or another method does not change the legibility, and in most situations, it is still possible to avoid unintended conversions.

To sum it up:

  • For generic code, use auto v = T(args…​), because it does not hide any constructor, it makes it explicit that it is calling a constructor or a conversion operator for fundamental types and does work better with enum class, the only drawback are arrays, but it is possible to specialize for them.

  • For non-generic code, use the best domain-specific model, as general guidelines:

    • {} and = {} for collection like objects (containers, initializer_list, optional, pair, tuple, …​) when initialising with the content of the container.

    • = with literal (numbers, boolean, lambdas, string literals, user-defined literals,…​), when assigning a literal to an object that models the same logical domain.

    • = with factory functions like std::make_unique. In general, factory functions can have a more descriptive name than the constructor of a class.

    • () for values that are used for computing (most probably all classes with invariants), or when calling a constructor.

Both auto and static_assert can help to avoid unintended conversions, partially solving one of the problems {} is trying to address.

{} without = might appear to be more uniform but

  • it is not clear what it is going to do. Using std::initializer_list, using std::initializer_list and a constructor, only calling a constructor, or using aggregate initialization.

  • it’s ergonomically harder to write (needs probably a key combination, most of the time Shift key or AltGr or even both, depending on the keyboard locale…​)

  • it’s harder to read for many use cases, especially for people who are not experts in C++

  • it’s harder to recognize an initialization inside a block of code

  • it hides the meaning of the initialization, in generic code we do not even know if we are calling a constructor with n parameters or one constructor with one parameter.

  • makes it impossible to call some constructors, and specializing is probably not possible. If it is possible, it is surely more complex than specializing the code arrays, as they probably already need a specialization.

  • It permits conversions that () does not permit.

Notice that the main selling point of {} is that it avoids implicit conversion between integral types, but that makes it inconsistent with how it works with all other types.

These rules, or better, guidelines, are more complex than just "use {} whenever you can", but I believe it leads to code that is more natural and easier to maintain. If you want to use the same syntax everywhere (I do not recommend it) in the whole codebase, use () except for arrays. If you use GCC and can live with its extensions, you can also use () for arrays and be consistent in all use cases.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.