The article presents real examples that will help a beginner in the new standard of the language to understand how to apply new features in practice.
Table of Contents
Introduction
In this article, we will talk about the new features of the new C++17 standard.
The article presents real examples that will help a beginner in the new standard of the language to understand faster how to apply new features in practice.
The code in the sections was tested in Visual Studio 2017.
(You can click on the image to view the enlarged, original image here.)
Settings an Integrated Development Environment (IDE)
Regardless of the IDE, you will most likely have to configure it to work with the latest C ++ standard.
I work with Visual Studio 2017, so I’ll show you how to set it up to work with the new standard:
Set C++ Version
To set on compiler option for the Visual Studio project, follow these steps:
- In the Solution Explorer window, right-click the project name, and then choose Properties to open the project Property Pages dialog (or press ALT + ENTER).
- Select the Configuration properties > C/C++ > Language property page.
- In the property list, select the drop-down for the "Conformance mode" property, and then choose /permissive.
It will disable non-standard C++ extensions and will enable standard conformance in VS2017.
- In the property list, select the drop-down for the "C++ Language Standard" property, and then choose /std:c++17 or /std:c++latest.
- Press the "OK" button to save your changes.
To enable the latest features of the new standard, you can also take a different approach:
- Open the project's Property Pages dialog box.
- Select "Configuration Properties"-> "C/C++"->"Command Line".
- Add to "Additional Options" textbox following param: /std:c++17 or /std:c++latest
- Press the "OK" button to save your changes.
Structure Binding
Multiple return values from functions are not a new concept in programming and similar functionality is present in many other programming languages. C++17 comes with a new feature (structured bindings) that provides functionality similar to the multiple return values provided in other languages.
In the following example, I want to provide an overview of some of the options that we have in the old C++ standard, in the modern standard (C++11/14) and today in C++17 to return multiple values from functions:
1 #include <iostream>
2 #include <tuple> // std::tie
3
4 const double PI = 3.14159265;
5
6 void calculateSinCos(const double param, double & resSin, double & resCos)
7 {
8 resSin = sin(param * PI / 180.0); resCos = cos(param * PI / 180.0); }
11
12 std::pair<double, double> calculateSinCos(const double param)
13 {
14 return { sin(param * PI / 180.0), cos(param * PI / 180.0) };
15 }
16
17 std::tuple<double, double> calculateSinCos_Tuple(const double param)
18 {
19 return std::make_tuple(sin(param * PI / 180.0),
20 cos(param * PI / 180.0)); }
22
23 int main()
24 {
25 double param { 90.0 };
26 double resultSin { 0.0 };
27 double resultCos { 0.0 };
28
29 calculateSinCos(param, resultSin, resultCos);
31 std::cout << "C++98 : sin(" << param << ") = " <<
32 resultSin << ", cos(" << param << ") = "
33 << resultCos << "\n";
34
35 const auto resSinCos(calculateSinCos(param));
37 std::cout << "C++11 : sin(" << param << ") = " <<
38 resSinCos.first << ", cos(" << param << ") = "
39 << resSinCos.second << "\n";
40
41 std::tie(resultSin, resultCos) = calculateSinCos(param);
43 std::cout << "C++11 : sin(" << param << ") = " <<
44 resultSin << ", cos(" << param << ") = "
45 << resultCos << "\n";
46
47 auto[a, b] = calculateSinCos(param);
49 std::cout << "C++17 :
50 sin(" << param << ") = " << a << ",
51 cos(" << param << ") = " << b << "\n";
52
53 auto[x, y] =
55 calculateSinCos_Tuple(param); std::cout << "C++17 :
57 sin(" << param << ") = " << x << ",
58 cos(" << param << ") = " << y << "\n";
59 }
Let's look at the above code:
- In this approach:
calculateSinCos(param, resultSin, resultCos);
we used the oldest and possibly still most common method - using the OUTPUT params passed as reference can be used to "return" values to the caller.
- Consider the different way to access multiple values returned from functions:
const auto resSinCos(calculateSinCos(param));
Accessing the individual values of the resulting std::pair
by resSinCos.first
and resSinCos.second
was not very expressive, since we can easily confuse the names, and it’s hard to read.
- Alternatively, before C++17, it would be possible to use
std::tie
to unpack a tuple/pair to achieve a similar effect:
std::tie(resultSin, resultCos) = calculateSinCos(param);
This approach demonstrates how to unpack the resulting pair into two variables. Notice, this example shows all the power of std::tie
, but nonetheless, the std::tie
is less powerful than structured bindings, because we must first define all the variables we want to bind.
- Structured binding is a new functionality of C++17, making the code even more readable, expressive and concise.
auto[a, b] = calculateSinCos(param);
Notice, the variables a
and b
are not references; they are aliases (or bindings) to the generated object member variables. The compiler assigns a unique name to the temporary object.
- In C++11, the
std::tuple
container has been added to build a tuple that contains multiple return values. But neither C++11 nor C++14 does support an easy way to get elements in a std::tuple
directly from the tuple (Of course, we can unpack a tuple using std::tie
, but we still need to understand the type of each object and how many objects are this tuple. Phew, how painful it is...)
C++17 fixes this flaw, and the structured bindings allow us to write code as follows:
auto[x, y] = calculateSinCos_Tuple(param);
Let's see the output of the above code of structure binding:
As we can see, different approaches show the same result....
'if' and 'switch' Statements with Initializers
Good programming style limits the scope of variables. Sometimes, it is required to get some value, and only if it meets a certain condition can it be processed further.
For this purpose, C++17 provides a new version of the 'if
' statement with initializer.
if (init; condition)
Let's see how the 'if
' condition worked before the new C++17 standard:
#include <iostream>
int getValueOfNumber() {
return 5;
}
int main() {
int num = getValueOfNumber();
if (num > 100) {
std::cout << "The value of the num is greater than 100" << std::endl;
}
else {
std::cout << "The value of the num is less than or equal to 100" << std::endl;
}
std::cout << "num =" << num << std::endl;
}
Please notice that the num
value is visible inside the if
and else
statements, as well as OUTSIDE the scope conditions.
Now, in C++17, we can write:
#include <iostream>
int getValueOfNumber() {
return 5;
}
int main() {
if (auto num = getValueOfNumber(); num > 100) {
std::cout << "The value of the num is greater than 100" << std::endl;
}
else {
std::cout << "The value of the num is less than or equal to 100" << std::endl;
}
std::cout << "num =" << num;
}
If we try to compile the above code, we will get the following error:
Now, num
is visible only INSIDE the if
and else
statements, so accessing a variable outside the scope of if
/else
causes an error...
The same applies to switch
statements.
C++17 provides new version of the 'switch
' statement with initializers.
switch (init; condition)
Let's see how the 'switch
' condition worked before the new C++17 standard:
#include <iostream>
#include <cstdlib>
#include <ctime>
int getRandomValueBetween_1_and_2() {
srand(time(NULL));
return rand() % 2 + 1; }
int main() {
int num = getRandomValueBetween_1_and_2();
switch (num) {
case 1:
std::cout << "num = 1 \n"; break;
case 2:
std::cout << "num = 2 \n"; break;
default:
std::cout << "Error value in num ! \n";
}
std::cout << "Value output outside the 'switch': num =" << num << std::endl;
}
Please notice that the num
value is visible inside the switch
statements, as well as OUTSIDE the scope conditions.
Now, in C++17, we can write:
#include <iostream>
#include <cstdlib>
#include <ctime>
int getRandomValueBetween_1_and_2() {
srand(time(NULL));
return rand() % 2 + 1; }
int main() {
switch (auto num(getRandomValueBetween_1_and_2()); num) {
case 1:
std::cout << "num = 1 \n"; break;
case 2:
std::cout << "num = 2 \n"; break;
default:
std::cout << "Error value in num ! \n";
}
std::cout << "Value output outside the 'switch': num =" << num << std::endl;
}
If we try to compile the above code, we will get the following error:
Now, num is visible only INSIDE the switch
statements, so accessing a variable outside the scope of switch
causes an error...
Due to the described mechanism, the scope of the variable remains short. Before C++17, this could only be achieved with additional {curly braces}.
The short lifetimes reduce the number of variables in scope, keeping code clean and making refactoring easier.
Thus, this new C++17 feature is very useful for further use.
Constexpr Lambdas and Capturing *this by Value
As it is written here: "In C++11 and later, a lambda expression—often called a lambda—is a convenient way of defining an anonymous function object (a closure) right at the location where it is invoked or passed as an argument to a function. Typically, lambdas are used to encapsulate a few lines of code that are passed to algorithms or asynchronous methods. This article defines what lambdas are, compares them to other programming techniques, describes their advantages, and provides a basic example."
C++17 offers two significant improvements to lambda expressions:
- constexpr lambdas
- capture of *this
constexpr lambdas
Lambda expressions are a short form for writing anonymous functors introduced in C++11, which have become an integral part of modern C++ standard. Using the constexpr keyword, also introduced in C++11, we can evaluate the value of a function or variable at compile time. In C++17, these two entities are allowed to interact together, so lambda can be invoked in the context of a constant expression.
For example:
constexpr auto my_val {foo()};
Declaring the variable my_val
with the constexpr
modifier will ensure that the function object it stores will be created and initialized at compile time.
In the definition of a lambda expression, we can use the optional constexpr
parameter:
[capture clause] (parameter list) mutable costexpr exception-specification -> return-type
{
}
If we explicitly mark the lambda expression with the constexpr keyword, then the compiler will generate an error when this expression does not meet the criteria of the constexpr function. The advantage of using constexpr
functions and lambda expressions is that the compiler can evaluate their result at compile time if they are called with parameters that are constant throughout the process. This will result in less code in the binary later. If we do not explicitly indicate that lambda expressions are constexpr
, but these expressions meet all the required criteria, then they will be considered constexpr
anyway, only implicitly.
For example:
constexpr auto NvalueLambda = [](int n) { return n; };
If we want the lambda expression to be constexpr
, then it is better to explicitly set it as such, because in case of errors the compiler will help us identify them by printing error messages.
Let's look at an example demonstrating how C++17 evaluates statically (at compile time) lambda expressions and functions:
#include <iostream>
constexpr int Increment(int value) {
return [value] { return value + 1; }();
};
constexpr int Decrement(int value) {
return [value] { return value - 1; }();
};
constexpr int AddTen(int value) {
return [value] { return value + 10; }();
};
int main() {
constexpr auto SumLambda = [](const auto &a, const auto &b)
{ return a + b; }; constexpr auto NvalueLambda = [](int n) { return n; }; constexpr auto Pow2Lambda = [](int n) { return n * n; };
auto Add32Lambda = [](int n)
{
return 32 + n;
};
auto GetStr = [](std::string s)
{
return "Hello" + s;
};
constexpr std::string(*funcPtr)(std::string) = GetStr;
static_assert(13 == SumLambda(8,5), "SumLambda does not work correctly");
static_assert(12 == NvalueLambda(12), "NvalueLambda does not work correctly");
static_assert(42 == Add32Lambda(10), "Add32Lambda does not work correctly");
static_assert(25 == Pow2Lambda(5), "Pow2Lambda does not work correctly");
static_assert(11 == Increment(10), "Increment does not work correctly");
static_assert( 9 == Decrement(10), "Decrement does not work correctly");
static_assert(25 == AddTen(15), "AddTen does not work correctly");
constexpr int resultAdd32 = Add32Lambda(10);
std::cout << "SumLambda(8,5) = " << SumLambda(8, 5) << std::endl;
std::cout << "NvalueLambda(12) = " << NvalueLambda(12) << std::endl;
std::cout << "Add32Lambda(10) = " << Add32Lambda(10) << std::endl;
std::cout << "Pow2Lambda(5) = " << Pow2Lambda(5) << std::endl;
std::cout << "Increment(10) = " << Increment(10) << std::endl;
std::cout << "Decrement(10) = " << Decrement(10) << std::endl;
std::cout << "DAddTen(15) = " << AddTen(15) << std::endl;
std::cout << "funcPtr("" World"") = " <<
funcPtr(" World").c_str() << std::endl;
return 0;
}
As we can see in the last few lines of the program, the static_assert
function performs compile-time assertion checking. Static assertions are a way to check if a condition is true
when the code is compiled. If it isn’t, the compiler is required to issue an error message and stop the compiling process. Thus, we check the lambda expressions at the compile time! If the condition is TRUE
, the static_assert
declaration has no effect. If the condition is FALSE
, the assertion fails, the compiler displays the error message and the compilation fails.
Since all our lambda expressions follow the rules, no errors produced. The program compiles well and executing the program, we will see the following output:
Capturing *this by Value
In C++17, we can use 'this
' pointer inside lambda-expressions used by member functions of a class, to capture a copy of the entire object of the class.
For example, the syntax of a lambda expression might be as follows:
auto my_lambda = [*this]() { };
Let's imagine that we need to implement a class that will do complex arithmetic operations. It will have a member function that will add the number 10 to the original value, and will also display the result of addition.
To do this, we implement the following code:
#include <iostream>
class ArithmeticOperations {
public:
ArithmeticOperations(const int val = 0) : m_sum(val) {}
void addTen() {
auto addTenLambda = [this]() { m_sum += 10;
std::cout << "\nFrom 'addTenLambda' body : the value of m_sum =
" << m_sum << "\n\n"; };
addTenLambda();
}
int getSum() {
return m_sum;
}
private:
int m_sum;
};
int main() {
ArithmeticOperations oper;
std::cout << "Before calling addTen() value of m_sum = " << oper.getSum() << '\n';
oper.addTen();
std::cout << "After calling addTen() value of m_sum = " << oper.getSum() << '\n';
return 0;
}
As we can see in ArithmeticOperations::addTen
, we use a lambda expression that captures 'this
' pointer. By capturing the 'this
' pointer, we effectively give the lambda expression access to all members that the surrounding member function has access to.
That is, function call operator of addTenLambda
will have access to all protected
and private
members of ArithmeticOperations
, including the member variable m_sum
.
Therefore, we can see that after calling addTenLambda
in the addTen
function, the value of m_sum
has changed.
However, maybe someone wants to not change the original value of the data member, but just get the result of adding 10 to our previous value.
And then, we can take advantage of the new C++17 functionality - capturing a copy of the original object, and carry out operations to change the values of class members already with this copy.
#include <iostream>
class ArithmeticOperations {
public:
ArithmeticOperations(const int val = 0) : m_sum(val) {}
void addTen_CaptureCopy() {
auto addTenLambdaCopy = [*this]() mutable
{ m_sum += 10; std::cout << "\nFrom 'addTenLambdaCopy'
body : the value of m_sum = " << m_sum << "\n\n"; };
addTenLambdaCopy();
}
int getSum() {
return m_sum;
}
private:
int m_sum;
};
int main() {
ArithmeticOperations oper;
std::cout << "Before calling addTen_CaptureCopy() value of m_sum =
" << oper.getSum() << '\n';
oper.addTen_CaptureCopy();
std::cout << "After calling addTen_CaptureCopy() value of m_sum =
" << oper.getSum() << "\n\n";
return 0;
}
In the example above, we reference a lambda expression with explicitly capture *this
to get a copy of this and have access to all data members of original object.
After capturing, we used the lambda expression body to modify the m_sum
member of copy of the original this.
Thus, after adding 10
to (* this).m_sum
directly in the lambda expression, we show the new value of (* this).m_sum
on the screen.
However, when we access getSum()
from the main function, we get the m_sum
value of the original object that was not changed. That is the difference!
This technique can be used, for example, in cases where it is likely that the lambda expression will outlive the original object. For example, when using asynchronous calls and parallel processing.
if constexpr
In C++17, the expressions 'if constexpr
' appeared. The new feature works just like regular if
-else
constructs. The difference between them is that the value of a conditional expression is determined at compile time.
To better illustrate what a great innovation 'if constexpr
' constructs are for C++17, let's look at how similar functionality was implemented with std::enable_if in previous language standards:
#include <iostream>
#include <string>
template <typename T>
std::enable_if_t<std::is_same<T, int>::value, void>
TypeParam_Cpp11(T x) {
std::cout << "TypeParam_Cpp11: int : " << x << std::endl;
}
template <typename T>
std::enable_if_t<std::is_same<T, double>::value, void>
TypeParam_Cpp11(T x) {
std::cout << "TypeParam_Cpp11: double : " << x << std::endl;
}
template <typename T>
std::enable_if_t<std::is_same<T, std::string>::value, void>
TypeParam_Cpp11(T x) {
std::cout << "TypeParam_Cpp11: std::string : " << x << std::endl;
}
int main() {
TypeParam_Cpp11(5.9);
TypeParam_Cpp11(10);
TypeParam_Cpp11(std::string("577"));
return 0;
}
The output of the above code is as follows:
Implementations of the three above TypeParam_Cpp11
functions look simple. Only functions complicate the following expressions std::enable_if_t <condition, type>
(C++14 adds a variation of std::enable_if_t
. It's just an alias for accessing the ::type
inside std::enable_if_t
) If condition is true
(i.e., only if std::is_same
is true
, and so T
and U
is the same type), std::enable_if
has a public
member typedef
type, otherwise, std::enable_if_t
does not refer to anything. Therefore, the condition can only be true
for one of the three implementations at any given time.
When the compiler sees different template functions with the same name and must select one of them, an important principle comes into play: it is indicated by the abbreviation SFINAE (Substitution Failure Is Not An Error). In this case, this means that the compiler does not generate an error if the return value of one of the functions cannot be inferred based on an invalid template expression (that is, std::enable_if_t
when the condition is false
). It will simply continue working and try to process other function implementations. That’s the whole secret. Uff... so much fuss!
Now let's change the above implementation using the new C++17 feature, that is, the ‘if constexpr
’ expressions, which allows us to simplify the code by making decisions at compile time:
#include <iostream>
#include <string>
template <typename T>
void TypeParam_Cpp17(T x) {
if constexpr (std::is_same_v<T, int>) {
std::cout << "TypeParam_Cpp17: int : " << x << std::endl;
}
else if constexpr (std::is_same_v<T, double>) {
std::cout << "TypeParam_Cpp17: double : " << x << std::endl;
}
else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "TypeParam_Cpp17: std::string : " << x << std::endl;
}
}
int main() {
TypeParam_Cpp17(5.9);
TypeParam_Cpp17(10);
TypeParam_Cpp17(std::string("577"));
return 0;
}
The output of the above code:
The above example uses conditional judgment. The constexpr
-if
-else
block, which has several conditions, completes the branching judgment at compile time. To determine the type of the argument 'x
', the TypeParam
function uses the expression std::is_same_v<A, B>
. The expression std::is_same_v<A, B>
is evaluated to the boolean value true if A and B
are of the same type, otherwise the value is false
.
At large, all run-time code generated by the compiler from a program will not contain additional branches related to the 'if constexpr
' conditions. It may seem that these mechanisms work the same way as the #if
and #else
preprocessor macros, which are used to substitute text, but in this construct, the whole code does not even need to be syntactically correct. Branches of the 'if constexpr
' construct must be syntactically valid, but unused branches need not be semantically correct.
__has_include
C++ 17 has a new feature for testing available headers. The new standard makes it possible to use macro constant preprocessor tokens or preprocessing expressions __has_include
to check whether a given header exists.
To avoid re-including the same file and infinite recursion, header protection is usually used:
#ifndef MyFile_H
#define MyFile_H
#endif
However, we can now use the constant expression of the preprocessor as follows:
#if __has_include (<header_name>)
#if __has_include ("header_name")
For example, in cases where we need to write portable code and at compile time check which of the headers we need to get, depending on the operating system, instead of using code like this:
#ifdef _WIN32
#include <tchar.h>
#define the_windows 1
#endif
#if (defined(linux) || defined(__linux) || defined(__linux__) ||
defined(__GNU__) || defined(__GLIBC__)) && !defined(_CRAYC)
#include <unistd.h>
#endif
#include <iostream>
int main() {
#ifdef the_windows
std::cout << "This OS is Windows";
#else
std::cout << "This OS is Linux";
#endif
return 0;
}
In С++17, we can use __has_include()
macro for the same purpose:
#if __has_include(<tchar.h>)
#include <tchar.h>
#define the_windows 1
#endif
#if __has_include(<unistd.h>)
#include <unistd.h>
#endif
#include <iostream>
int main() {
#ifdef the_windows
std::cout << "This OS is Windows";
#else
std::cout << "This OS is Linux";
#endif
return 0;
}
Using Attribute Namespaces Without Repetition
In C++17, when using “non-standard” attributes, the rules were slightly optimized.
In C++11 and C++14, if we need to write several attributes, it was necessary to specify an attached prefix for EACH namespace, something like this:
[[ rpr::kernel, rpr::target(cpu,gpu) ]] int foo() {
return 0;
}
Of course, the code above seems a bit cluttered and bloated. Therefore, the International Organization for Standardization decided to simplify the case in which it is necessary to use several attributes together.
In C++17, we no longer have to add prefix for each namespace with subsequent attributes being used together.
For example, using this innovation, the code shown above will look much clearer and more understandable:
[[using rpr: kernel, target(cpu,gpu)]] int foo() {
return 0;
}
Attributes 'nodiscard', 'fallthrough', 'maybe_unused'
In C++11, common attributes were added to the language allowing the compiler to provide additional information about characters or expressions in the code.
In the past, leading compilers have proposed their own mechanisms for this, such as:
__attribute__(deprecated)
in GNU GCC and LLVM/Clang __declspec(deprecated)
in Visual C++
Generic attributes, which are also series of attribute specifiers, allow us to express the same in a way that is independent of the compiler.
New Attributes in C++17
In C++17, three new attributes got into the standard, which allow us to control the appearance of various compiler warnings.
fallthrough
is placed before the case
branch in the switch
and indicates that operator break is intentionally omitted at this point, that is, the compiler should not warn of fallthrough.
void Test_With_fallthrough_Attribute(int num_version) {
switch (num_version) {
case 1998:
std::cout << "c++ 98";
break;
case 2011:
std::cout << "c++ 11" << std::endl;
break;
case 2014:
std::cout << "c++ 14" << std::endl;
case 2017:
std::cout << "c++ 17" << std::endl;
[[fallthrough]]; case 2020:
std::cout << "c++ 20" << std::endl;
break;
default:
std::cout << "Error!" << std::endl;
break;
}
}
Nevertheless, the Microsoft C++ compiler currently does not warn on fallthrough behavior, so this attribute has no effect on compiler behavior.
nodiscard
indicates that the value returned by the function cannot be ignored and must be stored in some variable
Let's look at the following example:
int Test_Without_nodiscard_Attribute() {
return 5;
}
[[nodiscard]] int Test_With_nodiscard_Attribute() {
return 5;
}
int main() {
Test_Without_nodiscard_Attribute();
Test_With_nodiscard_Attribute();
return 0;
}
After compilation, the following warning will be returned to call Test_With_nodiscard_Attribute ()
on the function:
"warning C4834: discarding return value of function with 'nodiscard' attribute
", as shown in the following image:
What it means is that you can force users to handle errors.
Let's fix the above code:
int Test_Without_nodiscard_Attribute() {
return 5;
}
[[nodiscard]] int Test_With_nodiscard_Attribute() {
return 5;
}
int main() {
Test_Without_nodiscard_Attribute();
int res = Test_With_nodiscard_Attribute();
return 0;
}
maybe_unused
causes the compiler to suppress warnings about a variable that is not used in some compilation modes (for example, the function return code is only checked in assert).
Let's look at the following example:
[[maybe_unused]] void foo() { std::cout <<
"Hi from foo() function \n"; }
int main() {
[[maybe_unused]] int x = 10;
return 0;
}
In the above example, the function foo ()
and the local variable 'x
' are not used, however warning about unused variables will be suppressed.
In Visual Studio 2017 version 15.3 and later (available with /std:c++17) [[maybe_unused]] specifies that a variable, function, class, typedef, non-static data member, enum, or template specialization may be intentionally not used.
The compiler does not warn when an entity marked [[maybe_unused]] is not used.
std::string_view
C++17 brings us a new type called std::string_view, a type defined in the string_view
header, added to the Standard Library.
Due to the performance of this type, it is recommended to use it instead of const std::string&
for input string parameters. Values of this type will act analogously to values of type const std::string
only with one major difference: the strings they encapsulate can never be modified through their public interface. In other words, std::string_view
gives us the ability to refer to an existing string in a non-owning way, we can view but not touch the characters of std::string_view
(while there is nothing wrong with using const std::string_view
&, we can as well pass std::string_view
by value because copying these objects is cheap).
Let's take a look at the following example:
#include <iostream>
#include <string_view>
#include <chrono>
void func_str(const std::string & s) {
std::cout << "s =" << s.data() << std::endl;
}
void func_str_view(std::string_view s) {
std::cout << "s =" << s.data() << std::endl;
}
int main() {
std::string str ("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
auto t1 = std::chrono::high_resolution_clock::now();
func_str(str);
auto t2 = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
std::cout << "The processing time of func_str(str) is " << duration1 << " ticks\n\n";
auto t3 = std::chrono::high_resolution_clock::now();
func_str_view(str);
auto t4 = std::chrono::high_resolution_clock::now();
auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(t4 - t3).count();
std::cout << "The processing time of func_str_view(str) is " << duration2 << " ticks\n\n";
return 0;
}
Let's take a look at the output of the above code:
Of course on your machine, the output may be different, but in any case, the above example shows that the function using std::string_view
is much faster. Note the func_str_view
function accepting some string
, but does not need ownership, it clearly reflects the intention: the function gets an overview of the string
. The func_str_view
function will also work with const char *
arguments, because std::string_view
is a thin view of a character array, holding just a pointer and a length. Therefore, this allows us to provide just one method that can efficiently take either a const char*
, or a std::string
, without unnecessary copying of the underlying array.
For example, we can call a function this way:
func_str_view("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
This func_str_view
function doesn't own anything, it just looks at the string
and does not involve any heap allocations and deep copying of the character array.
That’s why this function works much faster than func_str
.
Meanwhile, std::string_view
also has a number of limitations compared to its "kinsman" - std::string
:
std::string_view
knows nothing of null-termination
Unlike std::string
, std::string_view
does not support the c_str ()
function, which will give us an array of characters with a null
-terminated. Of course, std::string_view
has the data()
function, but like data()
of std::string
, this does not guarantee that the character array will be null
-terminated.
Therefore, to call a function with a string null
-terminated argument:
func_str_view_c_str("aaaaabbbbbccccc");
We need to create a temporary string
std::string
in the function body, for example, like this:
void func_str_view_c_str(std::string_view s) {
std::cout << "s = " << std::string(s).c_str()
<< std::endl; }
- Returning
std::string_view
from a function can lead to a problem of dangling pointers:
Let's look at the following example:
#include <iostream>
#include <string_view>
class StringMngr {
std::string m_str;
public:
StringMngr(const char* pstr = "Test") :m_str(pstr) {}
std::string_view GetSubString() const {
return m_str.substr(1u); }
};
int main() {
StringMngr str_mgr;
std::cout << "str_mgr.GetSubString() = " << str_mgr.GetSubString();
return 0;
}
The output of the above code is as shown below:
How does the above code work? What could be the problem?
We just want to give the right to review the substring
, but the problem is that std::string::substr()
returns a temporary object of type std::string
.
And the method returns an overview (i.e., view) of the time line, which will disappear by the time the overview can be used.
The correct solution would be to explicitly convert to std::string_view
before calling substr
:
std::string_view GetSubString() const {
return std::string_view(m_str).substr(1u);
}
After modifying the GetSubString()
function, we get the expected output:
The substr()
method for std::string_view
is more correct: it returns an overview of the substring without creating a temporary copy.
The real problem is that, ideally, the std::string::substr()
method should return std::string_view
.
And this is just one aspect of the more general problem of dangling references that has not been resolved in C++.
Nested 'namespaces'
Namespaces are used to organize code into logical groups, to group different entities such as: classes, methods and functions under a name. In addition, namespaces designed to prevent name collisions that can occur especially when your code base includes multiple libraries. All identifiers at namespace scope are visible to one another without qualification. OK, this is understandable, and before C++17, we used namespaces like this:
#include <iostream>
namespace CompanyABC
{
class Transport
{
public:
void foo() { std::cout << "foo()"; }
};
void goHome1() { std::cout << "goHome1()"; }
void startGame() { std::cout << "goHome1()"; }
}
Things are getting interesting when we use two or more nested namespaces.
Before C++ 17, we used the following namespace format:
#include <iostream>
namespace CompanyDEF {
namespace GroundTransportation {
namespace HeavyTransportation {
class Transport {
public:
void foo() { std::cout << "foo() \n"; }
};
void goHome() { std::cout << "goHome() \n"; }
}
void startGame() { std::cout << "startGame() \n"; }
}
}
Entities are created and called as follows:
CompanyDEF::GroundTransportation::HeavyTransportation::Transport trnsprt;
trnsprt.foo();
CompanyDEF::GroundTransportation::HeavyTransportation::goHome();
CompanyDEF::GroundTransportation::startGame();
Starting with C++17, nested namespaces can be written more compactly:
#include <iostream>
namespace CompanyDEF {
namespace GroundTransportation {
namespace HeavyTransportation {
class Transport {
public:
void foo() { std::cout << "foo() \n"; }
};
void goHome() { std::cout << "goHome() \n"; }
}
void startGame() { std::cout << "startGame() \n"; }
}
}
Entities are created and called as follows:
using namespace CompanyDEF::GroundTransportation::HeavyTransportation;
using namespace CompanyDEF::GroundTransportation::HeavyTransportation;
using namespace CompanyDEF::GroundTransportation;
Transport trnsprt;
trnsprt.foo();
goHome();
startGame();
String Conversions
Of course, in previous standards of C++, there were always string conversion functions, such as: std::atoi
, std::atol
, std::atoll
, std::stoi
, std::stol
, std::stoll
, std::sprintf
, std::snprintf
, std::stringstream
, std::sscanf
, std::strtol
, std::strtoll
, std::stringstream
, std::to_string
, etc. Unfortunately, these features are not efficient enough in terms of performance. It is generally believed that the above functions are slow for complex string processing, such as for efficient processing of complex data formats such as JSON or XML. Especially when such data formats are used for communication over a network where high bandwidth is a critical factor.
In C++17, we get two sets of functions: from_chars
and to_chars
, which allow low-level string conversions and significantly improve performance.
For example, Microsoft reports that C++17 floating-point to_chars()
has been improved for scientific notation, it is approximately 10x as fast as sprintf_s() “%.8e”
for float
s, and 30x as fast as sprintf_s() “%.16e”
for double
s. This uses Ulf Adams’ new algorithm, Ryu.
Note: If you are using Visual Studio 2017, you need to make sure that you have version 15.8 or older installed, since version 15.7 (with the first full C++17 library support) does not supports <charconv> header.
Let's look at an example of how, using std::from_chars
, we can convert strings to integers and floating-points numbers:
#include <iostream>
#include <string>
#include <array>
#include <charconv> // from_chars
int main()
{
std::cout << "---------------------------------------------------------------------------------------------------\n";
std::cout << "Let's demonstrate string conversion to integer using the 'std::from_chars' function " << std::endl;
std::cout << "---------------------------------------------------------------------------------------------------\n";
std::array<std::string, 8=""> arrIntNums = { std::string("-123"),
std::string("-1234"),
std::string("-12345"),
std::string("-1234567890987654321"),
std::string("123"),
std::string("1234"),
std::string("1234567890"),
std::string("1234567890987654321")};
int stored_int_value { 0 };
for (auto& e : arrIntNums)
{
std::cout << "Array element = " << e << std::endl;
const auto res = std::from_chars(e.data(), e.data() + e.size(),
stored_int_value );
switch (res.ec)
{
case std::errc():
std::cout << "Stored value: " << stored_int_value << ",
Number of characters = " << res.ptr - e.data() << std::endl;
break;
case std::errc::result_out_of_range:
std::cout << "Result out of range!
Number of characters = " << res.ptr - e.data() << std::endl;
break;
case std::errc::invalid_argument:
std::cout << "Invalid argument!" << std::endl;
break;
default:
std::cout << "Error: res.ec = " << int(res.ec) << std::endl;
}
}
std::cout << "\n---------------------------------------------------------------------------------------------------";
std::cout << "\nLet's demonstrate string conversion to double using the 'std::from_chars' function ";
std::cout << "\n---------------------------------------------------------------------------------------------------\n";
std::array<std::string, 8=""> arrDoubleNums = { std::string("4.02"),
std::string("7e+5"),
std::string("A.C"),
std::string("-67.90000"),
std::string("10.9000000000000000000000"),
std::string("20.9e+0"),
std::string("-20.9e+1"),
std::string("-10.1") };
double stored_dw_value { 0.0 };
for (auto& e : arrDoubleNums)
{
std::cout << "Array element = " << e << std::endl;
const auto res = std::from_chars(e.data(), e.data() + e.size(),
stored_dw_value, std::chars_format::general);
switch (res.ec)
{
case std::errc():
std::cout << "Stored value: " << stored_dw_value << ",
Number of characters = " << res.ptr - e.data() << std::endl;
break;
case std::errc::result_out_of_range:
std::cout << "Result out of range! Number of characters = "
<< res.ptr - e.data() << std::endl;
break;
case std::errc::invalid_argument:
std::cout << "Invalid argument!" << std::endl;
break;
default:
std::cout << "Error: res.ec = " << int(res.ec) << std::endl;
}
}
return 0;
}
Note that on the second call to std::from_chars
, we use the extra, last std::chars_format::general
argument.
The std::chars_format::general
(which is defined in header <charconv>
) used to specify floating-point formatting for std::to_chars
and std::from_chars
functions.
Let's look at the output of the above code, converting strings to an integers and doubles:
Now let's do the opposite action, i.e., consider an example of how we can use std::to_chars
to convert integers and floating point numbers into strings:
#include <iostream>
#include <string>
#include <array>
#include <charconv> // to_chars
int main()
{
std::cout << "--------------------------------------------------------------------------------------------------------------------\n";
std::cout << "Let's demonstrate the conversion of long values to a string (5 characters long) using the 'std::to_chars' function " << std::endl;
std::cout << "--------------------------------------------------------------------------------------------------------------------\n";
std::array<long, 8=""> arrIntNums = { -123,-1234,-12345, -1234567890, 123, 1234, 1234567890, 987654321};
std::string stored_str_value{ "00000" };
for (auto& e : arrIntNums)
{
stored_str_value = "00000";
std::cout << "Array element = " << e << std::endl;
const auto res = std::to_chars(stored_str_value.data(),
stored_str_value.data() + stored_str_value.size(), e );
switch (res.ec)
{
case std::errc():
std::cout << "Stored value: " << stored_str_value << ",
Number of characters = " << res.ptr - stored_str_value.data() << std::endl;
break;
case std::errc::result_out_of_range:
std::cout << "Result out of range! Number of characters = "
<< res.ptr - stored_str_value.data() << std::endl;
break;
case std::errc::value_too_large:
std::cout << "Value too large!" << std::endl;
break;
default:
std::cout << "Error: res.ec = " << int(res.ec) << std::endl;
}
}
std::cout << "--------------------------------------------------------------------------------------------------------------------\n";
std::cout << "Let's demonstrate the conversion of double values to a string (5 characters long) using the 'std::to_chars' function " << std::endl;
std::cout << "--------------------------------------------------------------------------------------------------------------------\n";
std::array<double, 8=""> arrDoubleNums = {4.02, 7e+5, 5, -67.90000,
10.9000000000000000000101,20.9e+0,-20.9e+1,-10.1};
for (auto& e : arrDoubleNums)
{
stored_str_value = "00000";
std::cout << "Array element = " << e << std::endl;
const auto res = std::to_chars(stored_str_value.data(),
stored_str_value.data() + stored_str_value.size(), e, std::chars_format::general);
switch (res.ec)
{
case std::errc():
std::cout << "Stored value: " << stored_str_value << ",
Number of characters = " << res.ptr - stored_str_value.data() << std::endl;
break;
case std::errc::result_out_of_range:
std::cout << "Result out of range! Number of characters = "
<< res.ptr - stored_str_value.data() << std::endl;
break;
case std::errc::value_too_large:
std::cout << "Value too large!" << std::endl;
break;
default:
std::cout << "Error: res.ec = " << int(res.ec) << std::endl;
}
}
return 0;
}
Let's take a look at the output of the above code, which converts long and double values to strings:
Over-Aligned Dynamic Memory Allocation
C++17 introduced dynamic memory allocation for over-aligned data.
The /Zc:alignedNew (C++17 over-aligned allocation) article perfectly describes how the compiler and MSVC library support C++17 standard dynamic memory allocation, so I won’t add anything on this issue.
Fold Expressions
C++17 standard introduces a new element of the language syntax - fold expressions. This new syntax is for folding variadic templates parameter pack (variadic template get variable number of arguments and is supported by C++ since the C++11). Using folding helps to avoid cumbersome recursive calls and allows us to apply the operations to all individual arguments of the pack in a compact form. When processing a list of template parameter pack, fold expressions can be used with the following binary operators: +, -, *, /, %, ^, &, |, =, <, >, <<, >>, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=,==, !=, <=, >=, &&, ||, ,, .*, ->*.
The syntax for declaring a fold expressions with variadic templates:
template<class... T>
decltype(auto) summation(T... Values)
{
return (Values + ...);
}
Let's look at the following example:
#include <iostream>
#include <string>
template <typename ... Ts>
auto Sum_RightHand(Ts ... ts)
{
return (ts + ...);
}
template <typename ... Ts>
auto Sum_LeftHand(Ts ... ts)
{
return (... + ts);
}
int main() {
std::cout << "Sum_RightHand output: \n";
std::cout << Sum_RightHand(10, 20, 30) << std::endl;
std::cout << Sum_RightHand(1.5, 2.8, 3.2) << std::endl;
std::cout << Sum_RightHand(std::string("Hi "), std::string("standard "),
std::string("C++ 17")) << std::endl;
std::cout << "\n";
std::cout << "Sum_LeftHand output: \n";
std::cout << Sum_LeftHand(10, 20, 30) << std::endl;
std::cout << Sum_LeftHand(1.5, 2.8, 3.2) << std::endl;
std::cout << Sum_LeftHand(std::string("Hi "), std::string("standard "),
std::string("C++ 17")) << std::endl;
return 0;
}
The output of the above code is as shown below:
In the above code, for both functions, we defined the signature using the template parameter pack:
template <typename ... Ts>
auto function_name (Ts ... ts)
Functions unfold all parameters and summarize them using a fold expression.(In the scope of functions, we used the +operator to apply to all values of the parameter pack)
As we can see, Sum_RightHand (ts + ...)
and Sum_LeftHand (... + ts)
give identical results. However, there is a difference between them, which may matter in other cases: if the ellipsis (...) is on the right side of the operator, an expression is called a right fold. If it is on the left side, it is the left fold.
In our example, Sum_LeftHand
is unfolded as follows:
10 + (20 + 30)
1.5 + (2.8 + 3.2)
"Hi" + ("standard " + "C++ 17")
and the right unary fold with Sum_RightHand
is unfolded as follows:
(10 +20) + 30
(1.5 + 2.8) + 3.2
("Hi" + "standard ") + "C++ 17"
The functions work with different data types: int
, double
, and std::string
, but they can be called for any type that implements the +operator.
In the above example, we passed some parameters to both functions to get the result of summation. But what happens if we call functions without parameters?
For example like this:
int main() {
std::cout << Sum_RightHand() << std::endl;
return 0;
}
By calling the Sum_RightHand ()
function without arguments, an arbitrary length parameter pack will not contain values that can be folded, and this results in compilation errors.
To solve the problem, we need to return a certain value. An obvious solution would be to return zero.
So we can implement it like this:
#include <iostream>
#include <string>
template <typename ... Ts>
auto Sum_RightHand(Ts ... ts) {
return (ts + ... + 0);
}
template <typename ... Ts>
auto Sum_LeftHand(Ts ... ts) {
return (0 + ... + ts);
}
int main() {
std::cout << "Sum_LeftHand() = " << Sum_LeftHand() << std::endl;
std::cout << "Sum_RightHand() = " << Sum_RightHand() << std::endl;
return 0;
}
The output of the above code:
Note that both fold expressions use the initial value ZERO! When there are no arguments, neutral elements are very important - in our case, adding any number to zeros does not change anything, making 0 a neutral element. Therefore, we can add 0
to any fold expressions using the +
or -
operators. If the parameter pack is empty, this will cause the function to return the value 0
. From a mathematical point of view, this is correct, but in terms of implementation, we need to determine what is right depending on our requirements.
'inline' Variables
C++17 standard gives us the ability to declare inline variables.
This feature makes it much easier to implement header-only libraries (where the full definitions of all macros, functions and classes comprising the library are visible to the compiler in a header file form).
Notice the C++11 introduced us to the non-static data member, with which we can declare and initialize member variables in one place:
class MyDate {
int m_year { 2019 };
int m_month { 11 };
int m_day { 10 };
std::string strSeason{ "Winter" };
};
However, before C++17, we could not initialize the value of static variables data member directly in the class. We must perform initialization outside of the class.
Let's look at a simple example of the 'Environment
' class, which can be part of a typical header-only library.
#include <iostream>
class Environment {
public:
static const std::string strVersionOS { "Windows" };
};
Environment environementManager;
Below the class definition, we defined a global class object to access the static data member of the Environment
class.
If we try to compile the above file, we get the following error:
What happened here and why is the compiler complaining?
The Environment
class contains a static
member and at the same time, it is globally accessible itself, which led to a double-defined symbols when included from multiple translation units. When we turned it on multiple C++ source files in order to compile and link them, it didn’t work out.
To fix this error, we need to add the inline keyword, which would not have been possible before C++17:
#include <iostream>
class Environment {
public:
static const inline std::string strVersionOS { "Widows" };
};
inline Environment environementManager;
It's all...
Previously, only methods/functions could be declared as inline, but C++17 allows us to declare inline variables as well.
In the example above, we made a declaration and definition in one place, but this can be done in different places, for example, like this:
#include <iostream>
class Environment {
public:
static const std::string strVersionOS;
};
inline const std::string Environment::strVersionOS = "Windows";
inline Environment environementManager;
Library Additions
Many new and useful data types have been added to the Standard Library in C++17, and some of them originated in Boost.
std::byte
std::byte
represents a single byte. Developers have traditionally used char
(signed or unsigned) to represent bytes, but now there is a type that can be not only a character or an integer.
However, a std::byte
can be converted to an integer and vice versa. The std::byte
type is intended to interact with the data warehouse and does not support arithmetic operations, although it does support bitwise operations.
To illustrate the above, let's look at the following code:
#include <iostream>
#include <cstddef>
void PrintValue(const std::byte& b) {
std::cout << "current byte value is " << std::to_integer<int>(b) << std::endl;
}
int main() {
std::byte bt {2};
std::cout << "initial value of byte is " << std::to_integer<int>(bt) << std::endl;
bt <<= 2;
PrintValue(bt);
}
The output of the above code:
std::variant
A std::variant
is a type-safe union that contains the value of one of the alternative types at a given time (there cannot be references, arrays, or 'void
' here).
A simple example: suppose there is some data where a certain company can be represented as an ID
or as a string
with the full name of this company. Such information can be represented using a std ::variant
containing an unsigned integer or string
. By assigning an integer to a std::variable
, we set the value, and then we can extract it using std::get
, like this:
#include <iostream>
#include <variant>
int main() {
std::variant<uint32_t, std::string> company;
company = 1001;
std::cout << "The ID of company is " << std::get<uint32_t>(company) << std::endl;
return 0;
}
The output of the above code:
If we try to use a member that is not defined in this way (e.g., std::get<std::string>(company)
), the program will throw an exception.
Why use std::variant
instead of the usual union? This is mainly because unions are present in the language primarily for compatibility with C and do not work with objects that are not POD types.
This means, in particular, that it is not easy to put members with custom copy constructors and destructors in a union. With std::variant
, there are no such restrictions.
std::optional
The type std::optional
is an object that may or may not contain a value; this object is useful and convenient to use as the return value of a function when it cannot return a value; then it serves as an alternative, for example, to a null
pointer. When working with optional, we gain an additional advantage: now the possibility of a function failure is explicitly indicated directly in the declaration, and since we have to extract the value from optional, the probability that we accidentally use a null
value is significantly reduced.
Let's take a look at the following example:
#include <iostream>
#include <string>
#include <optional>
std::optional<int> StrToInt(const std::string& s) {
try {
int val = std::stoi(s);
return val;
}
catch (std::exception&) {
return {};
}
}
int main() {
int good_value = StrToInt("689").value_or(0);
std::cout << "StrToInt(""689"") returns " << good_value << std::endl;
int bad_value = StrToInt("hfjkhjjkgdsd").value_or(0);
std::cout << "StrToInt(""hfjkhjjkgdsd"") returns " << bad_value << std::endl;
return 0;
}
The output of the above code:
The above example shows a StrToInt
function that attempts to turn a string
into an integer. By returning std::optional
, the StrToInt
function leaves the possibility that an invalid string
can be passed, which cannot be converted. In the main
, we use value_or()
function to get the value from std::optional
, and if the function fails, it returns the default value of zero (in case the conversion failed).
std::any
One another addition to C++17 is the type std::any
. std::any
provides a type-safe container for a single value of any type, and provides tools that allow you to perform type-safe validation.
Let's look at the following example:
#include <any>
#include <utility>
#include <iostream>
#include <vector>
int main() {
std::vector<std::any> v { 10, 20.2, true, "Hello world!" };
for (size_t i = 0; i < v.size(); i++) {
auto& t = v[i].type();
if (t == typeid(int)) {
std::cout << "Index of vector : " << i << " Type of value : 'int'\n";
}
else if (t == typeid(double)) {
std::cout << "Index of vector : " << i << " Type of value : 'double'\n";
}
else if (t == typeid(bool)) {
std::cout << "Index of vector : " << i << " Type of value : 'bool'\n";
}
else if (t == typeid(char *)) {
std::cout << "Index of vector : " << i << " Type of value : 'char *'\n";
}
}
std::cout << "\n std::any_cast<double>(v[1]) = " << std::any_cast<double>(v[1])
<< std::endl;
return 0;
}
The output of the above code is as follows:
In the above code, in the loop, we pass through the elements std::vector<std::any>
. At each iteration, we extract one of the elements of the vector and then try to determine the real type of std::any
values.
Please notice that std::any_cast<T>(val)
returns a copy of the internal T
value in 'val
'. If we need to get a reference to avoid copying complex objects, we need to use the any_cast<T&>(val)
construct.
But the double
type is not a complex object and therefore we can afford to get a copy. This is exactly what we did, in the penultimate line of the above code, in which we got access to an object of type double
from v [1].
Filesystem
C++17 added a new library designed to greatly simplify working with file systems and their components, such as paths, files, and directories. Since every second program, one way or another, is working with the file system, we have a new functionality that saves us the tedious work with file paths in the file system. After all, some file paths are absolute, while others are relative, and perhaps they are not even direct, because they contain indirect addresses: . (current directory) and .. (parent directory).
For separating directories, the Windows OS uses a backslash (\), while Linux, MacOS, and various Unix-like operating systems use a slash (/)
The new functionality introduced in C++17 supports the same principle of operation for different operating systems, so we don't need to write different code fragments for portable programs that support different operating systems.
Note: If you use Visual Studio 2017, version 15.7 and later support the new C++17 <filesystem> standard. This is a completely new implementation that is not compatible with the previous std::experimental version. It was became possible by symlink support, bug fixes, and changes in standard-required behavior. At the present, including <filesystem> provides the new std::filesystem and the previous std::experimental::filesystem. Including <experimental/filesystem> provides only the old experimental implementation. The experimental implementation will be removed in the next
ABI (Application Binary Interface)-breaking release of the libraries.
The following example illustrates working with filesystem::path and filesystem::exists The std::filesystempath
class is of utmost importance in situations where we use the library associated with the Filesystem, since most functions and classes are associated with it. The filesystem::exists
function allows you to check if the specified file path actually exists.
Let's choose properties to open the project Property Pages in Visual Studio and select the Configuration properties > Debugging > Command Arguments and define the command line parameter: "C:\Test1" :
Then run the following code:
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cout << "Usage: " << argv[0] << " <path>\n";
return 1;
}
fs::path path_CmdParam { argv[1] };
if (fs::exists(path_CmdParam)) {
std::cout << "The std::filesystem::path " << path_CmdParam << " is exist.\n";
}
else {
std::cout << "The std::filesystem::path " << path_CmdParam << " does not exist.\n";
return 1;
}
return 0;
}
The path 'C:\Test1' does not exist on my machine and therefore the program prints the following output:
In the main function, we check whether the user provided a command-line argument. In case of a negative response, we issue an error message and display on the screen how to work with the program correctly. If the path to the file was provided, we create an instance of the filesystem::path
object based on it. We initialized an object of the path class based on a string that contains a description of the path to the file. The filesystem::exists
function allows you to check whether the specified file path actually exists. Until now, we can't be sure, because it's possible to create objects of the path class that don't belong to a real file system object. The exists
function only accepts an instance of the path class and returns true
if it actually exists. This function is able to determine which path we passed to it (absolute or relative), which makes it very usable.
In addition to the filesystem::exists
function, the filesystem
module also provides many useful functions for creating, deleting, renaming, and copying:
In the next example, we will illustrate working with absolute and relative file paths to see the strengths of the path class and its associated helper functions. The path class automatically performs all necessary string conversions. It accepts arguments of both wide and narrow character arrays, as well as the std::string
and std::wstring
types, formatted as UTF8 or UTF16.
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main(int argc, char *argv[]) {
fs::path pathToFile { L"C:/Test/Test.txt" };
std::cout << "fs::current_path() = "
<< fs::current_path() << std::endl
<< "fs::absolute (C:\\Test\\Test.txt) = "
<< fs::absolute(pathToFile) << std::endl
<< std::endl;
std::cout << "(C:\\Test\\Test.txt).root_name() = "
<< pathToFile.root_name() << std::endl
<< "(C:\\Test\\Test.txt).root_path() = "
<< pathToFile.root_path() << std::endl
<< "(C:\\Test\\Test.txt).relative_path() = "
<< pathToFile.relative_path() << std::endl
<< "(C:\\Test\\Test.txt).parent_path() = "
<< pathToFile.parent_path() << std::endl
<< "(C:\\Test\\Test.txt).filename() = "
<< pathToFile.filename() << std::endl
<< "(C:\\Test\\Test.txt).stem() = "
<< pathToFile.stem() << std::endl
<< "(C:\\Test\\Test.txt).extension() = "
<< pathToFile.extension() << std::endl;
fs::path concatenateTwoPaths{ L"C:/" };
concatenateTwoPaths /= fs::path("Test/Test.txt");
std::cout << "\nfs::absolute (concatenateTwoPaths) = "
<< fs::absolute(concatenateTwoPaths) << std::endl;
return 0;
}
The output of the above code:
The current_path() function returns the absolute path of the current working directory (in above example, the function returns the home directory on my laptop, since I started the application from there). Next, we perform various manipulations with fs::path pathToFile
.
The path
class has several methods that return info about different parts of the path itself, as opposed to the file system object that it can refer to.
- pathToFile.root_name() returns the root name of the generic-format path
- pathToFile.root_path() returns the root path of the path
- pathToFile.relative_path() returns path relative to root-path
- pathToFile.parent_path() returns the path to the parent directory
- pathToFile.filename() returns the generic-format filename component of the path
- pathToFile.stem() returns the filename identified by the generic-format path stripped of its extension
- pathToFile.extension() returns the extension of the filename
In the last three lines (before the return
statement), we can see how filesystem::path
also automatically normalizes path separators.
We can use a '/
' as a directory separator in constructor arguments. This allows us to use the same strings to store paths in both Unix and WINDOWS environments.
Now let's see how we can use <filesystem>
to make a list of all the files in a directory using recursive and non-recursive traversal of directory (s).
Take the following directory:
and run the following code:
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main(int argc, char *argv[]) {
fs::path dir{ "C:\\Test\\" };
if (!exists(dir)) {
std::cout << "Path " << dir << " does not exist.\n";
return 1;
}
for (const auto & entry : fs::directory_iterator(dir))
std::cout << entry.path() << std::endl;
return 0;
}
The output of the above code:
Pay attention that the for
loop uses the std::filesystem::directory_iterator introduced in C++17, which can be used as a LegacyInputIterator
that iterates over the directory_entry
elements of a directory (but does not visit the subdirectories). Nevertheless, our directory “C:\Test” contains child directories:
If we change in the above code, fs::directory_iterator
to std::filesystem::recursive_directory_iterator:
for (const auto & entry : fs::recursive_directory_iterator(dir))
std::cout << entry.path() << std::endl;
Then we get the following output after running the program:
std::filesystem::recursive_directory_iterator is a LegacyInputIterator
that iterates over the directory_entry
elements of a directory, and, recursively, over the entries of all subdirectories.
Also, as in the previous case, the iteration order is unspecified, except that each directory entry is visited only once.
We can also get a list of subdirectories using the std::copy_if algorithm which copies the elements in the range [begin,end)
for which ' [](const fs::path& path)
' returns true
to the range of 'subdirs
' vector, and using construction a std::back_inserter
inserts new elements at the end of container 'subdirs
' (see code below).
The std::copy
displays the resulting 'subdirs
' vector:
#include <iostream>
#include <filesystem>
#include <vector>
#include <iterator>
namespace fs = std::filesystem;
int main(int argc, char *argv[]) {
fs::path dir{ "C:\\Test\\" };
if (!exists(dir)) {
std::cout << "Path " << dir << " does not exist.\n";
return 1;
}
std::cout << "\nLet's show all the subdirectories " << dir << "\n";
std::vector<fs::path> paths;
for (const auto & entry : fs::recursive_directory_iterator(dir))
paths.push_back(entry.path());
fs::recursive_directory_iterator begin("C:\\Test");
fs::recursive_directory_iterator end;
std::vector<fs::path> subdirs;
std::copy_if(begin, end, std::back_inserter(subdirs), [](const fs::path& path) {
return fs::is_directory(path);
});
std::copy(subdirs.begin(), subdirs.end(),
std::ostream_iterator<fs::path>(std::cout, "\n"));
return 0;
}
Below is the output of a recursive traversal of the “C:\Test\” directory, which is presented in the above code:
In C++17, the <filesystem>
has a number of tools for obtaining metainformation about a file/directory and performing operations with file systems.
For example, we have the opportunity to get the file size, read or set the last time data was written by a process to a given file, read or set file permissions, etc.
Let's take a look at the directory from the previous example again:
#include <iostream>
#include <fstream>
#include <filesystem>
namespace fs = std::filesystem;
void demo_perms(fs::perms p)
{
std::cout << ((p & fs::perms::owner_read) != fs::perms::none ? "r" : "-")
<< ((p & fs::perms::owner_write) != fs::perms::none ? "w" : "-")
<< ((p & fs::perms::owner_exec) != fs::perms::none ? "x" : "-")
<< ((p & fs::perms::group_read) != fs::perms::none ? "r" : "-")
<< ((p & fs::perms::group_write) != fs::perms::none ? "w" : "-")
<< ((p & fs::perms::group_exec) != fs::perms::none ? "x" : "-")
<< ((p & fs::perms::others_read) != fs::perms::none ? "r" : "-")
<< ((p & fs::perms::others_write)!= fs::perms::none ? "w" : "-")
<< ((p & fs::perms::others_exec) != fs::perms::none ? "x" : "-")
<< '\n';
}
std::time_t getFileWriteTime(const std::filesystem::path& filename) {
#if defined ( _WIN32 )
{
struct _stat64 fileInfo;
if (_wstati64(filename.wstring().c_str(), &fileInfo) != 0)
{
throw std::runtime_error("Failed to get last write time.");
}
return fileInfo.st_mtime;
}
#else
{
auto fsTime = std::filesystem::last_write_time(filename);
return decltype (fsTime)::clock::to_time_t(fsTime);
}
#endif
}
int main(int argc, char *argv[]) {
fs::path file_Test1 { "C:\\Test\\Test1.txt" };
std::string line;
std::fstream myfile(file_Test1.u8string());
std::cout << "The " << file_Test1 << "contains the following value : ";
if (myfile.is_open())
{
while (getline(myfile, line)) {
std::cout << line << '\n';
}
myfile.close();
}
else {
std::cout << "Unable to open " << file_Test1 ;
return 1;
}
std::cout << "File size = " << fs::file_size(file_Test1) << std::endl;
std::cout << "File permissions = ";
demo_perms(fs::status(file_Test1).permissions());
std::time_t t = getFileWriteTime(file_Test1);
std::cout << file_Test1 << " write time is : "
<< std::put_time(std::localtime(&t), "%c %Z") << '\n';
return 0;
}
In the above code, we can see that with fs::file_size, we can determine the size of the file without reading its contents (as the program output shows, the file contains 5 characters: TEST1
, and this is exactly what the fs::file_size function calculated. To read or set file permissions, we use fs::status("C:\Test\Test1.txt").permissions() and the demo_perms
function taken from here. To demonstrate the last modification of the file "C:\Test\Test1.txt", we used the getFileWriteTime
function. As it is written here: clock::to_time_t (ftime)
does not work for MSVC, so the function uses a portable solution for _WIN32 (non-POSIX Windows) and other operating systems.
In the next example, we will show information about free space on the file system. The fs::space global function returns an object of type fs::space_info, which describes the amount of free space on the media on which the specified path is located. If we transfer several paths located on the same media, the result will be the same.
#include <iostream>
#include <fstream>
namespace fs = std::filesystem;
int main(int argc, char *argv[]) {
fs::space_info diskC = fs::space("C:\\");
std::cout << "Let's show information about disk C : \n";
std::cout << std::setw(15) << "Capacity"
<< std::setw(15) << "Free"
<< std::setw(15) << "Available"
<< "\n"
<< std::setw(15) << diskC.capacity
<< std::setw(15) << diskC.free
<< std::setw(15) << diskC.available
<< "\n";
return 0;
}
The output of the above code which displays information about free disk space:
The returned fs::space_info object contains three indicators (all in bytes):
- Capacity is a total size of the filesystem, in bytes
- Free is a free space on the filesystem, in bytes
- Available is a free space available to a non-privileged process (may be equal or less than free)
Parallel Algorithms
Support for parallel versions of most universal algorithms has been added to the C++17 standard library to help programs take advantage of parallel execution to improve performance. Almost every computer today has several processor cores, however, by default, in most cases, when calling any of the standard algorithms, only one of these cores is used, and the other cores do not participate in the operation of standard algorithms. C++17 fixes this situation and when processing large arrays or data containers, algorithms can work much faster, distributing the work among all available cores.
So, functions from <algorithm> working with containers have parallel versions. All of them received additional overload, which takes the first argument of the execution policy, that determines how the algorithm will be executed.
In C++17, the first parameter of the execution policy can take one of 3 values:
- std::execution::seq for normal sequential execution
- std::execution::par for normal parallel execution, in this mode, the programmer must take care to avoid the race state when accessing data, but can use memory allocation, mutex locks, and so on
- std::execution::par_unseq for unsequenced parallel execution, in this mode, the functors passed by the programmer should not allocate memory, block mutexes, or other resources
Thus, all we have to do, for instance, to get a parallel version of the std::sort
algorithm is tell the algorithm to use the so-called parallel execution policy, and just use one of the following options that is most suitable for a particular case:
std::sort(std::execution::par,
begin (name_of_container)), end (name_of_container)); std::sort(std::execution::seq,
begin (name_of_container)), end (name_of_container));
std::sort(std::execution::par_unseq,
begin (name_of_container)), end (name_of_container));
Let's look at the following example in which a std::vector
of 10,000 integers is sorted by std::sort
with and without parallelization:
#include <iostream>
#include <chrono>
#include <vector>
#include <algorithm>
#include <execution>
using namespace std;
using std::chrono::duration;
using std::chrono::duration_cast;
using std::chrono::high_resolution_clock;
void printVector(const char * pStatus, std::vector<int> &vect)
{
std::cout << "The vector with " << vect.size() << " elements "
<< pStatus << " sorting : \n";
for (int val : vect) {
std::cout << val << " ";
}
std::cout << "\n\n";
}
int main() {
const int numberOfElements = 10000;
const int numOfIterationCount = 5;
std::cout << "The number of concurrent threads supported is "
<< std::thread::hardware_concurrency() << "\n\n";
std::vector<int> vect(numberOfElements);
std::generate(vect.begin(), vect.end(), std::rand);
std::cout << "Let's sort the vector using sort() function WITHOUT PARALLELIZATION : \n";
for (int i = 0; i < numOfIterationCount; ++i) {
std::vector<int> vec_to_sort(vect);
const auto t1 = high_resolution_clock::now();
std::sort(vec_to_sort.begin(), vec_to_sort.end());
const auto t2 = high_resolution_clock::now();
std::cout << "The time taken to sot vector of integers is : "
<< duration_cast<duration<double, milli>>(t2 - t1).count() << "\n";
}
std::cout << "\n\n";
std::cout << "Let's sort the vector using sort() function
and a PARALLEL unsequenced policy (std::execution::par_unseq) : \n";
for (int i = 0; i < numOfIterationCount; ++i) {
std::vector<int> vec_to_sort(vect);
const auto t1 = high_resolution_clock::now();
std::sort(std::execution::par_unseq, vec_to_sort.begin(), vec_to_sort.end());
const auto t2 = high_resolution_clock::now();
std::cout << "The time taken to sot vector of integers is : "
<< duration_cast<duration<double, milli>>(t2 - t1).count() << "\n";
}
std::cout << "\n\n";
return 0;
}
The output of the above code is as follows:
In the above implementation, the parallel version of the algorithm will give a performance gain compared to the serial version only if the size of the range (numberOfElements
) exceeds a certain threshold, which can vary depending on the flags of the compilation, platform or equipment. Our implementation has an artificial threshold of 10,000 elements.
We can experiment with different threshold values and range sizes and see how this affects the execution time. Naturally, with only ten elements, we are unlikely to notice any difference.
However, when sorting large datasets, parallel execution makes a lot more sense, and the benefits can be significant.
The algorithms library also defines the for_each() algorithm, which we now can use to parallelize many regular range-based for
loops. However, we need to take into account that each iteration of the loop can execute independently of the other, otherwise you may run into data races.
Features That Have Been Removed From C++17
- Trigraphs
Trigraphs have been removed from C++17 because they are no longer needed.
In general, trigraphs were invented for terminals in which some characters are missing. As a result, instead of #define
, we can write ??= define
.
Trigraphs are replaced with the necessary characters at the very beginning, therefore these entries are equivalent. Instead of '{
', we can write '??<
', instead of '}
' use '??>
'.
They were used in C/C++ in the 80s because the old coding table did not support all the necessary characters ISO/IEC646, such as: table { border-collapse: collapse; } th, td { border: 1px solid orange; padding: 10px; text-align: left; }
Trigraph | Equivalent symbol |
??= | # |
??( | [ |
??/ | \ |
??) | ] |
??' | ^ |
??< | { |
??! | | |
??> | } |
??- | ~ |
Visual C++ still supports trigram substitution, but it is disabled by default. For information on how to enable trigram substitution, see here.
- Removing operator++ for bool
The operator++ for bool is deprecated and was removed in C++17.
Let's look at the following code:
#include <iostream>
int main() {
bool b1 = false;
b1++;
return 0;
}
After compiling the above code, we get the following errors:
- Error (active) E2788: incrementing a bool value is not allowed
- Compiler Error C2428: 'operation' : not allowed on operand of type '
bool
'
- Remove register keyword
The register keyword is deprecated even in the C++11 standard.
Only in C++17, it was decided to remove it, because it is not used for new compilers.
If we declare a register variable, it will just give a hint to the compiler. Can use it or not. Even if we do not declare the variable as register, the compiler can put it in the processor register.
When writing the following code in Visual Studio 2017 version 15.7 and later: (available with /std:c++17):
register int x = 99;
We will get the following warning: "warning C5033: 'register' is no longer a supported storage class
"
- Removing std::auto_ptr
The std::auto_ptr
was introduced in C++98, has been officially excluded from C ++ since C++17.
Let's look at the following code:
#include <iostream>
#include <memory>
int main() {
std::auto_ptr<int> p1(new int);
return 0;
}
After compiling the above code, we get the following errors:
Namespace "std
" has no member "auto_ptr
"...
- Removing dynamic exception specifications
The dynamic exception specification, or throw(optional_type_list) specification, was deprecated in C++11 and removed in C++17, except for throw()
, which is an alias for noexcept(true)
.
Let's see an example:
#include <iostream>
int foo(int x) throw(int) {
if (100 == x) {
throw 2;
}
return 0;
}
int main() {
auto x = foo(100);
return 0;
}
After compilation, the compiler will return the following warning: "warning C5040: dynamic exception specifications are valid only in C++14 and earlier treating as noexcept(false)
"
- Removing some deprecated aliases
std::ios_base::iostate
std::ios_base::openmode
std::ios_base::seekdir
- Removed allocator from std::function
The following prototypes were removed in C++17:
template< class Alloc >
function( std::allocator_arg_t, const Alloc& alloc ) noexcept;
template< class Alloc >
function( std::allocator_arg_t, const Alloc& alloc,std::nullptr_t ) noexcept;
template< class Alloc >
function( std::allocator_arg_t, const Alloc& alloc, const function& other );
template< class Alloc >
function( std::allocator_arg_t, const Alloc& alloc, function&& other );
template< class F, class Alloc >
function( std::allocator_arg_t, const Alloc& alloc, F f );
Summary
Bjarne Stroustrup (creator of C++): "C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off."
It is my sincere hope that the contents of this article will help someone avoid a "leg wound" :-)