5 Ways Using Braces Can Make Your C++ Code More Expressive
A lot of languages use braces to structure code. But in C++, braces are much more than mortar for holding blocks of code together. In C++, braces have meaning.
Or more exactly, braces have several meanings. Here are 5 simple ways you can benefit from them to make your code more expressive.
#1 Filling all sorts of containers
Before C++11, putting initial contents in an STL was a pain:
std::vector<std::string> words; words.push_back("the"); words.push_back("mortar"); words.push_back("for"); words.push_back("holding"); words.push_back("code"); words.push_back("together");
By using std::initializer_list
, C++11 brought a much expected syntax to write this sort of code easily, using braces:
std::vector<std::string> words = {"the", "mortar", "holding", "code", "together"};
This doesn’t just apply to STL containers. The braces syntax allows to intialize the standard collections that can carry different types, that is to say std::tuple
and std::pair
:
std::pair answer = {"forty-two", 42}; std::tuple cue = {3, 2, 1, "go!"};
This doesn’t rely on a std::initializer_list
though. This is just the normal passing of arguments to the constructor of std::pair
that expects two elements, and to the one of std::tuple
that accepts more.
Note that the particular above example uses C++17 type deduction in template class constructors, that allows not to write the types that the pair or tuple contains.
Those two syntaxes for initialization combine to initialize a map in a concise way:
std::map<int, std::string> numbers = { {1, "one"}, {2, "two"}, {3, "three"} };
Indeed, a std::map
is an STL container than contains std::pair
s.
#2 Passing composite arguments to a function
Suppose we have a function that displays the elements inside of a std::vector
, for example this display
function:
void display(std::vector<int> const& values) { if (!values.empty()) { std::cout << values[0]; for (size_t i = 1; i < values.size(); ++i) { std::cout << " - " << values[i]; } std::cout << '\n'; } }
Then we don’t always have to pass a std::vector
explicitly to this function. Instead, we can directly pass in a set of objects between braces as an argument to this function. For example, with this calling code:
display({1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
the program outputs:
1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10
This relies on the fact that the constructor of std::vector
that takes a std::initialiser_list
is not explicit
. Therefore, the function calls makes an implicit construction of the vector from the initializer_list.
Note that while it allows a nice syntax for a particular type such as std::vector<int>
, this would not work for template code. display
could be made generic here, by replacing int
withT
:
template<typename T> void display(std::vector<T> const& values) { if (!values.empty()) { std::cout << values[0]; for (size_t i = 1; i < values.size(); ++i) { std::cout << " - " << values[i]; } std::cout << '\n'; } }
But then the simple syntax:
display({1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
no longer compiles. Indeed, the type passed being std::initializer_list<int>
, it needs an implicit conversion to be turned into a std::vector<int>
. But the compiler cannot deduce a template type based on an implicit conversion.
If you know how to fix this code so that the simple syntax compiles without having to write std::vector<int>
in front of it, please let me know in a comment!
Also note that since std::pair
and std::tuple
don’t rely on std::initializer_list
, the passing only the contents as an argument to a function, without writing std::pair
or std::tuple
, doesn’t compile for them. Even if it would have been nice.
Indeed, if we adapt our display
function to display the contents of an std::pair
for example:
template<typename First, typename Second> void display(std::pair<First, Second> const& p) { std::cout << p.first << " - " << p.second << '\n'; }
The following call site would not compile:
display({1, 2});
The same holds for std::tuple
.
#3 Returning composite, objects from a function
We’ve seen that braces allowed to pass in collections to a function. Does it work in the other direction, to get collections out of a function? It turns out that it does, with even more tools at our disposal.
Let’s start with a function returning a std::vector
:
std::vector<int> numbers() { return {0, 1, 2, 3, 4, 5}; }
As the above code shows, we don’t have to write explicitly std::vector<int>
before the set of objects between braces. The implicit constructor takes care of building the vector that the function returns from the initializer_list
.
This example was symmetric to passing an STL container to a function. But in the case of std::pair
and std::tuple
, the situation is not as symmetric. Even though as seen above, we can’t just pass {1, 2}
a function that expects a std::pair<int, int>
, we can return it from it!
For example, the following function compiles and returns a pair with 5
and "five"
inside:
std::pair<int, std::string> number() { return {5, "five"}; }
No need to write std::pair
in front of the braces. Why? I don’t know. If you recognize which mechanism of C++ initialization is at play here, I’ll be grateful if you let me know in a comment.
#4 Aggregate initialization
An aggregate initialization consists in using a set of data between braces to initialize the members of a struct
or class
that doesn’t declare a constructor.
This works only under certain conditions, where the initialized type is of an ascetic simplicity: no constructor, no method, no inheritance, no private data, no member initializer. It must look like a bunch of data strung together:
struct Point { int x; int y; int z; };
Under those conditions, aggregate initialization kicks in, which lets us write the following syntax with braces to initialize the members of Point
:
Point p = {1, 2, 3};
Then p.x
is 1
, p.y
is 2
and p.z
is 3
.
This feature matters when you decide whether or not your struct
should have constructors.
#5 RAII }
When learning C++, I was stunned by all the things that could happen with this single line of code:
}
A closing brace closes a scope, and this calls the destructor of all the objects that were declared inside that scope. And calling the code of those destructors can do dozens of things, from freeing memory to closing a database handle to shutting down a file:
void f() { // scope opening std::unique_ptr<X> myResource = // ... ... } // scope closing, unique_ptr is destroyed, the underlying pointer is deleted
This is the fundamental C++ idiom of RAII. One of the virtues of RAII is to make your code more expressive, by offloading some bookkeeping operations to the destructors of objects instead of having your code burdened with it.
Smart pointers are a great example to illustrate the power of RAII. To go further with RAII, check out To RAII or not to RAII, that is the question.
Braces have meaning
How extensively do you use braces in your C++ code? Do you use them in other ways than the above 5 to make your code cleaner?
In C++, braces are not just simple syntactic delimiters between blocks of code. More than mortar of the codebase, they play the role of its inhabitants too. Take advantage of their idiomatic uses to make your code more expressive.
You may also like
- 3 Simple C++17 Features That Will Make Your Code Simpler
- Smart developers use smart pointers
- To RAII or Not to RAII?
- struct and Constructors in C++: an “It’s complicated” Relationship
Share this post!