C++17 In Detail

12 August 2019

17 Smaller but Handy C++17 Features

When you see an article about new C++ features, most of the time you’ll see a description of major elements. Looking at C++17, there are a lot of posts (including articles from this blog) about structured bindings, filesystem, parallel algorithms, if constexpr, std::optional, std::variant… and other prominent C++17 additions.

But how about those smaller parts? Library or language improvements that didn’t require decades to standardise or violent “battles” at the ISO meetings.

In this article, I’ll show you 17 smaller C++17 things that will improve your code.

The Language

Let’s start with the language changes first. C++17 brought larger features like structured bindings, if constexpr, folding expressions, updated expression evaluation order - I consider them as “significant” elements.

Yet, there are also smaller updates to the language that make it clearer and also allows you to write more compact code. Have a look below:

1. Dynamic Memory Allocation for Over-Aligned Data

If you work with SIMD instructions (for example to improve performance of some calculations, or in graphics engined, or in gamedev), you might often find some C-looking code to allocate memory.

For example aligned_malloc() or _aligned_malloc() and then aligned_free().

Why might you need those functions? It’s because if you have some specific types, like a Vec3 that has to be allocated to 128bits alignment (so it can fit nicely in SIMD registers), you cannot rely on Standard C++ new() functions.

struct alignas(16) Vec3 {
    float x, y, z;
};

auto ptr = new Vec3[10];

To work with SSE you require the ptr to be aligned to 16-byte boundary, but in C++14 there’s no guarantee about this.

I’ve even seen the following guides in CERT:

MEM57-CPP. Avoid using default operator new for over-aligned types - SEI CERT C++ Coding Standard - Confluence

Or here: Is there any guarantee of alignment of address return by C++’s new operation? - Stack Overflow.

Fortunately, the C++17 standard fixes this by introducing allocation functions that honour the alignment of the object.

For example we have:

void* operator new[](std::size_t count, std::align_val_t al);

Now, when you allocate an object that has a custom alignment, then you can be sure it will be appropriately aligned.

Here’s some nice description at MSVC pages: /Zc:alignedNew (C++17 over-aligned allocation).

2. Inline Variables

When a class contains static data members, then you had to provide their definition in a corresponding source file (in only one source file!).

Now, in C++17, it’s no longer needed as you can use inline variables! The compiler will guarantee that a variable has only one definition and it’s initialised only once through all compilation units.

For example, you can now write:

// some header file...
class MyClass {
    static inline std::string startName = "Hello World";
};

The compiler will make sure MyClass::startName is defined (and initialised!)) only once for all compilation units that include MyClass header file.

You can also read about global constants in a recent article at Fluent C++:
What Every C++ Developer Should Know to (Correctly) Define Global Constants where inline variables are also discussed.

3. __has_include Preprocessor Expression

C++17 offers a handy preprocessor directive that allows you to check if the header is present or not.

For example, GCC 7 supports many C++17 library features, but not std::from_chars, while GCC 9.1 is has updated support for that function.

With __has_include we can write the following code:

#if defined __has_include
#    if __has_include(<charconv>)
#        define has_charconv 1
#        include <charconv>
#    endif
#endif

std::optional<int> ConvertToInt(const std::string& str) {
    int value { };
    #ifdef has_charconv
        const auto last = str.data() + str.size();
        const auto res = std::from_chars(str.data(), last, value);
        if (res.ec == std::errc{} && res.ptr == last)
            return value;
    #else
        // alternative implementation...
    #endif

    return std::nullopt;
}

In the above code, we declare has_charconv based on the __has_include condition. If the header is not there, we need to provide an alternative implementation for ConvertToInt.

If you want to read more about __has_include, then see my recent article: Improve Multiplatform Code With __has_include and Feature Test Macros.

The Standard Library

With each release of C++, its Standard Library grows substantially. The Library is still not as huge as those we can use in Java or .NET frameworks, but still, it covers many useful elements.

Plus not to mention that we have boost libs, that serves as the Standard Library 2.0 :)

In C++17, a lot of new and updated elements were added. We have a big features like the filesystem, parallel algorithms and vocabulary types (optional, variant, any). Still, there are lots (and much more than 17) that are very handy.

Let’s have a look:

4. Variable Templates for Traits

In C++11 and C++14, we got many traits that streamlined template code. Now we can make the code even shorter by using variable templates.

All the type traits that yields ::value got accompanying _v variable templates. For example:

std::is_integral<T>::value has std::is_integral_v<T>

std::is_class<T>::value has std::is_class_v<T>

This improvement already follows the _t suffix additions in C++14 (template aliases) to type traits that “return” ::type.

One example:

// before C++17
template <typename Concrete, typename... Ts>
enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>>
constructArgsOld(Ts&&... params)
{
    return std::make_unique<Concrete>(forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete> >
constructArgsOld(...)
{
    return nullptr;
}

Can be shorten (along with using if constexpr) into:

template <typename Concrete, typename... Ts>
unique_ptr<Concrete> constructArgs(Ts&&... params)
{  
  if constexpr (is_constructible_v<Concrete, Ts...>)
      return make_unique<Concrete>(forward<Ts>(params)...);
   else
       return nullptr;
}

Also, if you want to create your custom trait that returns ::value, then it’s a good practice to provide helper variable template _v as well:

// define is_my_trait<T>...

// variable template:
template< class T >
inline constexpr bool is_my_trait_v = is_my_trait<T>::value;

5. Logical Operation Metafunctions

C++17 adds handy template metafunctions:

  • template<class... B> struct conjunction; - logical AND
  • template<class... B> struct disjunction; - logical OR
  • template<class B> struct negation; - logical negation

Here’s an example, based on the code from the proposal (P0006):

#include<type_traits>

template<typename... Ts>
std::enable_if_t<std::conjunction_v<std::is_same<int, Ts>...> >
PrintIntegers(Ts ... args) { 
    (std::cout << ... << args) << '\n';
}

The above function PrintIntegers works with a variable number of arguments, but they all have to be of type int.

6. std::void_t Transformation Trait

A surprisingly simple metafunction that maps a list of types into void:

template< class... >
using void_t = void;

Extra note: Compilers that don’t implement a fix for CWG 1558 (for C++14) might need a more complicated version of it.

The void_t technique was often used internally in the library implementations, so now we have this helper type in the standard library out of the box.

void_t is very handy to SFINAE ill-formed types. For example it might be used to detect a function overload:

void Compute(int &) { } // example function

template <typename T, typename = void>
struct is_compute_available : std::false_type {};

template <typename T>
struct is_compute_available<T, 
           std::void_t<decltype(Compute(std::declval<T>())) >> 
               : std::true_type {};

static_assert(is_compute_available<int&>::value);
static_assert(!is_compute_available<double&>::value);

is_compute_available checks if a Compute() overload is available for the given template parameter.

If the expression decltype(Compute(std::declval<T>())) is valid, then the compiler will select the template specialisation. Otherwise, it’s SFINEed, and the primary template is chosen (I described this technique in a separate article: How To Detect Function Overloads in C++17, std::from_chars Example).

7. std::from_chars

This function was already mentioned in previous items, so let’s now see what’s that all about.

from_chars gives you low-level support for text to number conversions! No exceptions (as std::stoi, no locale, no extra memory allocations), just a simple raw API to use.

Have a look at the simple example:

#include <charconv> // from_char, to_char
#include <iostream>
#include <string>

int main() {
    const std::string str { "12345678901234" };
    int value = 0;
    const auto res = std::from_chars(str.data(), 
                                     str.data() + str.size(), 
                                     value);

    if (res.ec == std::errc()) {
        std::cout << "value: " << value 
                  << ", distance: " << res.ptr - str.data() << '\n';
    }
    else if (res.ec == std::errc::invalid_argument) {
        std::cout << "invalid argument!\n";
    }
    else if (res.ec == std::errc::result_out_of_range) {
        std::cout << "out of range! res.ptr distance: " 
                  << res.ptr - str.data() << '\n';
    }
}

The example is straightforward, it passes a string str into from_chars and then displays the result with additional information if possible.

The API is quite “raw”, but it’s flexible and gives you a lot of information about the conversion process.

Support for floating-point conversion is also possible (at least in MSVC, but still not implemented in GCC/Clang - as of August 2019).

And if you need to convert numbers into strings, then there’s also a corresponding function std::to_chars.

8. Splicing for maps and sets

Let’s now move to the area of maps and sets, in C++17 there a few helpful updates that can bring performance improvements and cleaner code.

The first example is that you can now move nodes from one tree-based container (maps/sets) into other ones, without additional memory overhead/allocation.

Previously you needed to copy or move the items from one container to the other.

For example:

#include <set>
#include <string>
#include <iostream>

struct User {
    std::string name;

    User(std::string s) : name(std::move(s)) {
        std::cout << "User::User(" << name << ")\n";
    }
    ~User() {
        std::cout << "User::~User(" << name << ")\n";
    }
    User(const User& u) : name(u.name) { 
        std::cout << "User::User(copy, " << name << ")\n";
    }

    friend bool operator<(const User& u1, const User& u2) {
        return u1.name < u2.name;
    }
};

int main() {
    std::set<User> setNames;
    setNames.emplace("John");
    setNames.emplace("Alex");
    std::set<User> outSet;

    std::cout << "move John...\n";
    // move John to the outSet
    auto handle = setNames.extract(User("John"));
    outSet.insert(std::move(handle));

    for (auto& elem : setNames)
        std::cout << elem.name << '\n';

    std::cout << "cleanup...\n";
}

Output:

User::User(John)
User::User(Alex)
move John...
User::User(John)
User::~User(John)
Alex
cleanup...
User::~User(John)
User::~User(Alex)

In the above example, one element “John” is extracted from setNames into outSet. The extract method moves the found node out of the set and physically detaches it from the container. Later the extracted node can be inserted into a container of the same type.

Let’s see another improvement for maps:

9. try_emplace() Method

Here’s an example:

The behaviour of try_emplace is important in a situation when you move elements into the map:

int main() {
    std::map<std::string, std::string> m;
    m["Hello"] = "World";

    std::string s = "C++";
    m.emplace(std::make_pair("Hello", std::move(s)));

    // what happens with the string 's'?
    std::cout << s << '\n';
    std::cout << m["Hello"] << '\n';

    s = "C++";
    m.try_emplace("Hello", std::move(s));
    std::cout << s << '\n';
    std::cout << m["Hello"] << '\n';
}

The code tries to replace key/value["Hello", "World"] into ["Hello", "C++"].

If you run the example the string s after emplace is empty and the value “World” is not changed into “C++”!

try_emplace does nothing in the case where the key is already in the container, so the s string is unchanged.

10. insert_or_assign() Method

Another method is insert_or_assign().

It inserts a new object in the map or assigns the new value. But as opposed to operator[] it also works with non-default constructible types.

Also, the regular insert() method will fail if the element is already in the container, so now we have an easy way to express “force insertion”.

For example:

struct User {
    // from the previous sample...
};

int main() {
    std::map<std::string, User> mapNicks;
    //mapNicks["John"] = User("John Doe"); // error: no default ctor for User()

    auto [iter, inserted] = mapNicks.insert_or_assign("John", User("John Doe"));
    if (inserted)
        std::cout << iter->first << " entry was inserted\n";
    else 
        std::cout << iter->first << " entry was updated\n";
}

This one finishes the section about ordered containers.

11. Return Type of Emplace Methods

Since C++11 most of the standard containers got .emplace* methods. With those methods, you can create a new object in place, without additional object copies.

However, most of .emplace* methods didn’t return any value - it was void. Since C++17 this is changed, and they now return the reference type of the inserted object.

For example:

// since C++11 and until C++17 for std::vector
template< class... Args >
void emplace_back( Args&&... args );

// since C++17 for std::vector
template< class... Args >
reference emplace_back( Args&&... args );

This modification should shorten the code that adds something to the container and then invokes some operation on that newly added object.

For example: in C++11/C++14 you had to write:

std::vector<std::string> stringVector;

stringVector.emplace_back("Hello");
// emplace doesn't return anything, so back() needed
stringVector.back().append(" World");

one call to emplace_back and then you need to access the elements through back().

Now in C++17, you can have one liner:

std::vector<std::string> stringVector;    
stringVector.emplace_back("Hello").append(" World");

12. Sampling Algorithms

New algorithm - std::sample - that selects n elements from the sequence:

#include <iostream>
#include <random>
#include <iterator>
#include <algorithm>

int main() {
    std::vector<int> v { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::vector<int> out;
    std::sample(v.begin(),               // range start
                v.end(),                 // range end
                std::back_inserter(out), // where to put it
                3,                       // number of elements to sample
                std::mt19937{std::random_device{}()});

    std::cout << "Sampled values: ";
    for (const auto &i : out)
        std::cout << i << ", ";
}

Possible output:

Sampled values: 1, 4, 9, 

13. gcd(), lcm() and clamp()

The C++17 Standard extended the library with a few extra functions.

We have a simple functions like clamp , gcd and lcm :

#include <iostream>
#include <algorithm>  // clamp
#include <numeric>    // for gcm, lcm

int main() {
    std::cout << std::clamp(300, 0, 255) << ', ';   
    std::cout << std::clamp(-10, 0, 255) << '\n'; 

    std::cout << std::gcd(24, 60) << ', ';
    std::cout << std::lcm(15, 50) << '\n';    
}

you can find even more math functions, special math functions like reinam_zeta, besel, etc… in the following paper https://wg21.link/N1542

14. Shared Pointers and Arrays

Before C++17, only unique_ptr was able to handle arrays out of the box (without the need to define a custom deleter). Now it’s also possible with shared_ptr.

std::shared_ptr<int[]> ptr(new int[10]);

Please note that std::make_shared doesn’t support arrays in C++17. But this will be fixed in C++20 (see P0674 which is already merged into C++20)

Another important remark is that raw arrays should be avoided. It’s usually better to use standard containers.

So is the array support not needed? I even asked that question at Stack overflow some time ago:

c++ - Is there any use for unique_ptr with array? - Stack Overflow

And that rose as a popular question :)

Overall sometimes you don’t have the luxury to use vectors or lists - for example, in an embedded environment, or when you work with third-party API. In that situation, you might end up with a raw pointer to an array. With C++17, you’ll be able to wrap those pointers into smart pointers (std::unique_ptr or std::shared_ptr) and be sure the memory is deleted correctly.

15. std::scoped_lock

With C++11 and C++14 we got the threading library and many support functionalities.

For example, with std::lock_guard you can take ownership of a mutex and lock it in RAII style:

std::mutex m;

std::lock_guard<std::mutex> lock_one(m);
// unlocked when lock_one goes out of scope...

The above code works, however, only for a single mutex. If you wanted to lock several mutexes, you had to use a different pattern, for example:

std::mutex first_mutex;
std::mutex second_mutex;

// ...

std::lock(fist_mutex, second_mutex);
std::lock_guard<std::mutex> lock_one(fist_mutex, std::adopt_lock);
std::lock_guard<std::mutex> lock_two(second_mutex, std::adopt_lock);
// ..

With C++17 things get a bit easier as with std::scoped_lock you can lock several mutexes at the same time.

std::scoped_lock lck(first_mutex, second_mutex);

Removed Elements

C++17 not only added lots of elements to the language and the Standard Library but also cleaned up several places. I claim that such clean-up is also as “feature” as it will “force” you to use modern code style.

16. Removing auto_ptr

One of the best parts! Since C++11, we have smart pointers that properly support move semantics.

auto_ptr was an old attempt to reduce the number of memory-related bugs and leaks… but it was not the best solution.

Now, in C++17 this type is removed from the library, and you should really stick to unique_ptr, shared_ptr or weak_ptr.

Here’s an example where auto_ptr might cause a disc format or a nuclear disaster:

void PrepareDistaster(std::auto_ptr<int> myPtr) {
    *myPtr = 11;
}

void NuclearTest() {
    std::auto_ptr<int> pAtom(new int(10));
    PrepareDistaster(pAtom);
    *pAtom = 42; // uups!
}

PrepareDistaster() takes auto_ptr by value, but since it’s not a shared pointer, it gets the unique ownership of the managed object. Later, when the function is completed, the copy of the pointer goes out of scope, and the object is deleted.

In NuclearTest() when PrepareDistaster() is finished the pointer is already cleaned up, and you’ll get undefined behaviour when calling *pAtom= 32.

17. Removing Old functional Stuff

With the addition of lambda expressions and new functional wrappers like std::bind() we can clean up old functionalities from C++98 era.

Functions like bind1st()/bind2nd()/mem_fun(), were not updated to handle perfect forwarding, decltype and other techniques from C++11. Thus it’s best not to use them in modern code.

Here’s a list of removed functions from C++17:

  • unary_function()/pointer_to_unary_function()
  • binary_function()/pointer_to_binary_function()
  • bind1st()/binder1st
  • bind2nd()/binder2nd
  • ptr_fun()
  • mem_fun()
  • mem_fun_ref()

For example to replace bind1st/bind2nd you can use lambdas or std::bind (available since C++11) or std::bind_front that should be available since C++20.

// old:
auto onePlus = std::bind1st(std::plus<int>(), 1);
auto minusOne = std::bind2nd(std::minus<int>(), 1);
std::cout << onePlus(10) << ", " << minusOne(10) << '\n';

// a capture with an initializer
auto lamOnePlus = [a=1](int b) { return a + b; };
auto lamMinusOne = [a=1](int b) { return b - a; };
std::cout << lamOnePlus(10) << ", " << lamMinusOne(10) << '\n';

// with bind:
using namespace std::placeholders; 
auto onePlusBind = std::bind(std::plus<int>(), 1, _1);
std::cout << onePlusBind(10) << ',';
auto minusOneBind = std::bind(std::minus<int>(), _1, 1);
std::cout << minusOneBind(10) << '\n';

The example above shows one “old” version with bind1st and bind2nd and then provides two different approaches: with a lambda expression and one with std::bind.

Summary

The list is not full, and we can add more and more things, for example, I skipped std::launder, direct initialisation of enum classes, aggregate changes, or other removed features from the library.

The list consists of my picks… but what are yours?
What’s your favourite small feature that improved (or you think might improve) your code?

C++17 In Detail
© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.