Last Update:
17 Smaller but Handy C++17 Features
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:
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;
- logicalAND
template<class... B> struct disjunction;
- logicalOR
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:
- How to Use The Newest C++ String Conversion Routines - std::from_chars
- How to Convert Numbers into Text with std::to_char in C++17
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
member 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?
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: