Jonathan Boccara's blog

5 Ways Using Braces Can Make Your C++ Code More Expressive

Published November 15, 2019 - 0 Comments

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.

C++ braces

#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::pairs.

#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

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin