Table of Contents

When you see an article about new C++ features, most of the time you’ll have 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 some 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 (plus a few extra!) smaller C++17 things that will improve your code.

Last Update: 19th October 2020 (the std::invoke section, plus smaller fixes).

See the similar C++20 article: 20 Smaller yet Handy C++20 Features - C++ Stories.

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.

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 - Fast, Low-level Conversion s  

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 October 2020).

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

See my blog posts on those procedures:

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 extractmember function 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() Function  

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() Member Function for Maps  

Another new feature is insert_or_assign() - which is a new member function for std::map.

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() member function 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 Functions  

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

However, most of .emplace* functions 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() + lots of math functions  

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';    
}

What’s more, C++17 brings even more math functions - called special math functions like rieman_zeta, assoc_laguerre, hermite, and others in the following paper N1542 or see here Mathematical special functions - @cppreference.

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 = 42.

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.

Extra  

But there’s more good stuff!

std::invoke - Uniform Call Helper  

This feature connects with the last thing that I mentioned - the functional stuff. While C++17 removed something, it also offered some cool new things!

With std::invoke you get access to a magical INVOKE expression that was defined in the Standard since C++11 (or even in C++0x, TR1), but wasn’t exposed outside.

In short the expression INVOKE(f, t1, t2, ..., tN) can handle the following callables:

  • function objects: like func(arguments...)
  • pointers to member functions (obj.*funcPtr)(arguments...)
  • pointer to member data obj.*pdata

See the full definition here:[func.require]

Additionally, those calls can also be invoked with references to objects or even pointers (smart as well!), or base classes.

As you can see, this expression creates a nice abstraction over several options that you can “call” something. No matter if that’s a pointer to a member function, a regular callable object, or even a data member.

Since C++17 (proposed in N4169) theINVOKE expression is now exposed through std::invoke which is defined in the <functional> header.

Let’s see some examples:

The first one with a regular function call:

#include <functional>
#include <iostream>

int intFunc(int a, int b) { return a + b; }

int main(){
    // a regular function:
    std::cout << std::invoke(intFunc, 10, 12) << '\n';
    
    // a lambda:
    std::cout << std::invoke([](double d) { return d*10.0;}, 4.2) << '\n';
}

See the code @Wandbox

That was easy, and how about member functions:

#include <functional>
#include <iostream>

struct Animal {
    int size { 0 };
    
    void makeSound(double lvl) { 
        std::cout << "some sound at level " << lvl << '\n'; 
    }
};

int main(){
    Animal anim;
    
    // before C++17:   
    void (Animal::*fptr)(double) = &Animal::makeSound;
    (anim.*fptr)(12.1);
    
    // with std::invoke:
    std::invoke(&Animal::makeSound, anim, 12.2);
    
    // with a pointer:
    auto* pAnim = &anim;
    std::invoke(&Animal::makeSound, pAnim, 12.3);
}

Live code @Wandbox

And the last example with invoking a data member, this will simply return a value of that member.

#include <functional>
#include <iostream>
#include <memory>

struct Animal {
    int size { 0 };
};

int main(){
    Animal anim { 12 };
    std::cout << "size is: " << std::invoke(&Animal::size, anim) << '\n';
    auto ptr = std::make_unique<Animal>(10);
    std::cout << "size is: " << std::invoke(&Animal::size, ptr) << '\n';
}

Live code @Wandbox

As you can see std::invoke makes it easy to get a value of some callable object or even a data member using the same syntax. This is important when you want to create a generic code that needs to handle such calls.

As it appears std::invoke also become an essential part for of things called Projections in Ranges that are introduced in C++20. You can see an example in my other post about Ranges.

And one additional update, in C++17 std::invoke wasn’t defined as constexpr, but it’s now since C++20!

There’s an excellent presentation from STL if you want to know more: CppCon 2015: Stephan T. Lavavej “functional: What’s New, And Proper Usage" - YouTube

Summary  

It was a lot of reading… and I hope you found something useful to try and explore.

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

If you want to see other elements of C++17 you can read my book - C++17 in Detail - or see the list @cppreference.

See the similar C++20 article: 20 Smaller yet Handy C++20 Features - C++ Stories.

Back to you:

And how about your preferences? What’s your favourite small feature of C++17?