Time to refactor with C++17

In the following article I will present some examples of novelties for C++17, which can make your code more readable, faster and your life much easier.

|
   C/C++ 

Introduction

To stand still is to move backwards. Especially in IT world where one technology can be viable only for a couple of years we need to adapt and learn quickly to stay valuable to employers. Another aspect of following newest trends is that they are usually better than their predecessors as people don’t tend to change things when there is no place for improvement. In the following article I will present some examples of such novelties for C++17, which can make your code more readable, faster and your life much easier.

Structured Bindings

Everyone who has worked with C++ pairs knows that using them can a bit annoying as you have to reassign its elements to some more descriptive variables or access them via first and second members. Now let’s look at the following example, we have pair that represent geographical position and function that returns such pair when given latitude and longitude.

template<typename T>
using Position = std::pair<T, T>;

template<typename T>
auto get_point_from_coordinates(T latitude, T longitude)
{
    return Position<T>{ latitude, longitude };
}

In the next step we would like to take is to print latitude and longitude. This is how it could be done without C++17,

const auto current_positions = get_position_from_coordinates(60, 70);

std::count << "Current position is " << current_positions.first
           << " latitude and " << current_positions.second << " longitude degress\n";

and here is the version with structured binding.

const auto [latitude, longitude] = get_position_from_coordinates(60, 70);

std::count << "Current position is " << latitude
           << " latitude and " << longitude << " longitude degress\n";

This second version is more descriptive and requires less code to type, but you might think “That’s just a few words saved, why should I even bother?”. Well, the beauty of this feature is that it works also with tuples, arrays or even structures. So when to decide to change Position any of those things, code that retrieves latitude and longitude variables stays the same.

template<typename T>
using Position = std::array<T, 2>;

template<typename T>
using Position = std::tuple<T, T>;

template<typename T>
struct Position
{
    T latitude;
    T longitude;
};

You can also use structured binding in loops and C++17 conditional statements.

std::map<std::string, unsigned> cities_population
{
    { "Moscow", 12380664 },
    { "London", 8787892 },
    { "New York City", 8537673 }
};

for (const auto &[city, population] : cities_population)
{
    std::cout << "In << city " live " << population << " people.\n";
}

constexpr if

Those who had done some template programming surely know what SFINE is. It stands for Substitution Failure is not an Error and in short it is a mechanism embedded in C++ that let us drop unwanted template specializations without causing compile error. I am sure that everyone will agree with opinion that it is a great feature, but implementation is rather harsh. Let’s assume we want to write simple Variable class with add function able to handle scalars and vectors. It could be implemented like this:

template <typename Type>
class Variable
{
    Type val;

public:
    Variable(Type v) : val{v} {}

    template <typename Integral>
    std::enable_if_t<!std::is_same_v<Type, std::vector<Integral>>>
    add(Integral x)
    {
        val += x;
    }

    template <typename Integral>
    std::enable_if_t<std::is_same_v<Type, std::vector<Integral>>>
    add(Integral x)
    {
        for (auto& n : val)
            n += x;
    }
};

The problem here is that you must keep two add overloads. Return type with enable_if check does not look attractive and without C++ templates knowledge you can just guess how and why it works.

template <typename Type>
class Variable
{
    Type val;

public:
    Variable(Type v) : val{v} {}

    template <typename Integral>
    void add(Integral x)
    {
        if constexpr (std::is_same_v<Type, std::vector<Integral>>)
        {
            for (auto& n : val)
                n += x;
        }
        else
            val += x;
    }
};

Here is the same class implemented with C++17 features, now there is only one add function and it is much easier to guess what it does.

Fold expression

Variadic templates are very powerful feature introduced in C++11. They give us a way to pass arbitrary number of template parameters. Here is a simple print function example.

template <typename Type>
void print(Type&& t)
{
    std::cout << t;
}

template <typename Type, typename... Args>
void print(Type&&, Args&&... args)
{
    std::cout << t;
    print(std::forward<Args>(args)...);
}

The biggest inconvenience in passing multiple parameters is that you have to use recursion and in result write more than one function. Well that’s not entirely true, there is a workaround, but it looks ugly and hacky. You can take a look at screen below.

template <typename... Args>
void print(Args&&... args)
{
    (int[]){0, (void(std::cout << std::forward<Args>(args)), 0)...};
}

The trick here is taking the advantage of the fact that parameter pack can be expanded in initializer list so we create array and pass arguments to stream while doing so. Does it work? Yes. Should you ever consider doing that in production code? Not in my opinion. Finally we should take a peek at C++ 17 folding expression.

template<typename ...Args>
void print(Args&&... args)
{
    (std::cout << ... << args) << '\n';
}

This is a much simpler construct, it let us avoid defining other functions and use recursion explicitly. This feature allows a programmer to expand a parameter packs with ease, so I assume that variadic templates will be much more popular in C++17.

std::string_view

Ok, next in the line is std::string_view. Many people don’t understand purpose of this class and it really is pretty simple. It allows us to create a non-owing view into any kind on C++ strings, so there are two biggest advantages. Firstly, we don’t need to pay for a copy when we just want to "look" at a string. Secondly, we can get nearly no cost wrapper for C style string which will have an interface of const std::string. Now it is time for examples. Let’s assume we want to write a function accepting a string and returning its size.

std::size_t get_string_size(const std::string& str)
{
    return str.size();
}

It looks reasonable. We can pass both std::string and C style string and we can make use of std::string interface. But what will a compiler do when we pass C string? In this case it will create std::string object which will bind to const reference and hold a copy of actual string passed to function. To avoid that we could create another function taking pointer to char.

std::size_t get_string_size(const char* str)
{
    return std::strlen(str);
}

The problem here is that we have to keep two implementations. That doesn’t sound that bad unless we have to write dozens of similar functions. Another downside is that we can’t use handy std::string interface while dealing with C strings.

std::size_t get_string_size(const std::string_view str)
{
    return str.size();
}

C++17 gets rid of this issue with std::string_view. This class basically holds pointer and size of string, so it will never force a copy and it acts as const std::string. All we have to remember is that it does not own memory where string is located so we need to make sure that actual string is valid when we use std::string_view object.

Summary

In the above article I have tried to present a few examples of C++17 features. They do not really enable us to do new things but rather make old ones more elegant and pleasant. In my opinion it is worth the effort to make use of them on daily basis as we always need to assume that someone will have to work with our code. Such features make code shorter, more readable and easier to change and expand. All of these attributes let us do things quicker and in result be more productive.

Piotr Rut

Did you like this article?

Time to refactor with C++17