Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / C++17

C++ 17 New Features and Trick

Rate me:
Please Sign up or sign in to vote.
4.99/5 (39 votes)
13 Mar 2020CPOL38 min read 32.3K   2.5K   34   11
New features of the C++17 standard
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.

Simple copying

(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:

  1. 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).
  2. Select the Configuration properties > C/C++ > Language property page.
  3. 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.

  4. In the property list, select the drop-down for the "C++ Language Standard" property, and then choose /std:c++17 or /std:c++latest.
  5. Press the "OK" button to save your changes.

Simple copying

To enable the latest features of the new standard, you can also take a different approach:

  1. Open the project's Property Pages dialog box.
  2. Select "Configuration Properties"-> "C/C++"->"Command Line".
  3. Add to "Additional Options" textbox following param: /std:c++17 or /std:c++latest
  4. Press the "OK" button to save your changes.

Simple copying

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:

C++
  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); // converting degrees to radians
  9       resCos = cos(param * PI / 180.0); // converting degrees to radians
 10  }
 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));  // packing values into tuple
 21  }
 22  
 23  int main()
 24  {
 25       double param { 90.0 };
 26       double resultSin { 0.0 };
 27       double resultCos { 0.0 };
 28  
 29       //C++98
 30       calculateSinCos(param, resultSin, resultCos);
 31       std::cout << "C++98 : sin(" << param << ") = " <<
 32       resultSin << ", cos(" << param << ") = "
 33       << resultCos << "\n";
 34  
 35       //C++11
 36       const auto resSinCos(calculateSinCos(param));
 37       std::cout << "C++11 : sin(" << param << ") = " <<
 38       resSinCos.first << ", cos(" << param << ") = "
 39       << resSinCos.second << "\n";
 40  
 41       //C++11
 42       std::tie(resultSin, resultCos) = calculateSinCos(param);
 43       std::cout << "C++11 : sin(" << param << ") = " <<
 44       resultSin << ", cos(" << param << ") = "
 45       << resultCos << "\n";
 46  
 47       // C++17
 48       auto[a, b] =  calculateSinCos(param);
 49       std::cout << "C++17 :
 50       sin(" << param << ") = " << a << ",
 51       cos(" << param << ") = " << b << "\n";
 52  
 53       // C++17
 54       auto[x, y] =
 55          calculateSinCos_Tuple(param); // std::make_tuple(sin(val), cos(val));
 56       std::cout << "C++17 :
 57       sin(" << param << ") = " << x << ",
 58       cos(" << param << ") = " << y << "\n";
 59  }

Let's look at the above code:

  1. In this approach:
    C++
    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.

  2. Consider the different way to access multiple values returned from functions:
    C++
    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.

  3. Alternatively, before C++17, it would be possible to use std::tie to unpack a tuple/pair to achieve a similar effect:
    C++
    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.

  4. Structured binding is a new functionality of C++17, making the code even more readable, expressive and concise.
    C++
    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.

  5. 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:

    C++
    auto[x, y] = calculateSinCos_Tuple(param);

Let's see the output of the above code of structure binding:

Output 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.

C++
if (init; condition)

Let's see how the 'if' condition worked before the new C++17 standard:

C++
#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:

C++
#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:

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.

C++
switch (init; condition)

Let's see how the 'switch' condition worked before the new C++17 standard:

C++
#include <iostream>
#include <cstdlib>
#include <ctime>
     
int getRandomValueBetween_1_and_2() {
     srand(time(NULL));
     return rand() % 2 + 1; // // value in the range 1 to 2
}

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:

C++
#include <iostream>
#include <cstdlib>
#include <ctime>
     
int getRandomValueBetween_1_and_2() {
     srand(time(NULL));
     return rand() % 2 + 1; // value in the range 1 to 2
}

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:

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:

C++
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:

C++
constexpr auto NvalueLambda = [](int n) { return n; };   // implicitly constexpr

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:

C++
#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; };            // implicitly constexpr
	constexpr auto NvalueLambda = [](int n) { return n; };        // implicitly constexpr
	constexpr auto Pow2Lambda   = [](int n) { return n * n; };    // implicitly constexpr
	
	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);  // implicitly constexpr
	
	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:

Output constexpr Lambda Expressions

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:

C++
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:

C++
#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;
}

Output of lambda-capture-this

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.

C++
#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;
}

Output of lambda-capture-copy-this

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:

C++
#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:

Output with std::enable_if_t

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:

C++
#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:

Output with 'if constexpr'

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:

C++
#ifndef MyFile_H 
#define MyFile_H
// contents of the file here
#endif  

However, we can now use the constant expression of the preprocessor as follows:

C++
#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:

C++
#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:

C++
#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:

C++
[[ rpr::kernel, rpr::target(cpu,gpu) ]] // repetition
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:

C++
[[using rpr: kernel, target(cpu,gpu)]] // without repetition
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.

  1. 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.
    C++
    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]];// No warning! The hint to the compiler
              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.

  2. 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:

    C++
    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:

    Error

    What it means is that you can force users to handle errors.

    Let's fix the above code:

    C++
    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;
    }
    
  3. 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:

    C++
    [[maybe_unused]] void foo() { std::cout <<
    "Hi from foo() function \n"; } // Warning suppressed for function not called
    
    int main() {
    
         [[maybe_unused]] int x = 10; // Warning suppressed for x
    
         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:

C++
#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:

The string_view output

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:

C++
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:

  1. 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:

    C++
    func_str_view_c_str("aaaaabbbbbccccc");
    
    We need to create a temporary string std::string in the function body, for example, like this:
    C++
    void func_str_view_c_str(std::string_view  s) {
         std::cout << "s = " << std::string(s).c_str()
                   << std::endl; // string_view does not provide a c_str()
    }
    
  2. Returning std::string_view from a function can lead to a problem of dangling pointers:

    Let's look at the following example:

    C++
    #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); // substring starts at index 1 till the end
         }
    };
    
    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:

    Substring

    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:

    C++
    std::string_view GetSubString() const {
         return std::string_view(m_str).substr(1u);
    }
    

    After modifying the GetSubString() function, we get the expected output:

    Substring

    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:

C++
#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:

C++
#include <iostream>

namespace CompanyDEF {
     namespace  GroundTransportation {
          namespace HeavyTransportation {

               class Transport {
               public:
                    void foo() { std::cout << "foo() \n"; }
               };

               void goHome() { std::cout << "goHome() \n"; }
          } // HeavyTransportation

          void startGame() { std::cout << "startGame() \n"; }

     } // roundTransportation

} // CompanyDEF 

Entities are created and called as follows:

C++
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:

C++
#include <iostream>

namespace CompanyDEF {
     namespace  GroundTransportation {
          namespace HeavyTransportation {

               class Transport {
               public:
                    void foo() { std::cout << "foo() \n"; }
               };

               void goHome() { std::cout << "goHome() \n"; }
          } // HeavyTransportation

          void startGame() { std::cout << "startGame() \n"; }

     } // roundTransportation

} // CompanyDEF 

Entities are created and called as follows:

C++
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 floats, and 30x as fast as sprintf_s() “%.16e” for doubles. 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:

C++
#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 /*, int base = 10*/);

		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:

from_chars (int)

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:

C++
#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";
     
     // construction uses aggregate initialization
     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 /*, int base = 10*/);

          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";
     
     // construction uses aggregate initialization
     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:

std::to_chars (long and double)

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:

C++
template<class... T>
decltype(auto) summation(T... Values)
{
	// unfolds into the expression Values1 + (Values2 + (Values3 + Values4))...
	return (Values + ...);
}

Let's look at the following example:

C++
#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:

Right and left fold

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:

C++
int main() {

     std::cout << Sum_RightHand() << std::endl;
     return 0;
}

Fold expressions error

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:

C++
#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:

Fold expressions zerro

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:

C++
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.

C++
#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:

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:

C++
#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:

C++
#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:

C++
#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::byte

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:

C++
#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:

std::byte

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:

C++
#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:

std::optional

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:

C++
#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:

std::any

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" :

command lie argument

Then run the following code:

C++
#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:

exists(file) 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.

C++
#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:

File path manipulation output

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:

The TestFolder

and run the following code:

C++
#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:

The content of Test folder

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:

The TestFolder

If we change in the above code, fs::directory_iterator to std::filesystem::recursive_directory_iterator:

C++
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:

The content of Test folder

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:

C++
#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:

All the subdirectories of Test folder

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:

The content of Test folder with details

C++
#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;
}

Details of file

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.

C++
#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:

Output 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:

C++
std::sort(std::execution::par,
begin (name_of_container)), end (name_of_container)); //same thing as the
                                                      //version without an execution policy
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:

C++
#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);

	//printVector("before (original vector)", vect);
 
	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);
		//printVector("before", vec_to_sort);
		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";
		//printVector("after", vec_to_sort);
	}

	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);
		// printVector("before", vec_to_sort);
		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";
		// printVector("after", vec_to_sort);
	}

	std::cout << "\n\n";
    
	return 0;
}

The output of the above code is as follows:

The comparing the normal operation of the sort function and the parallel version of the sort function

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

  1. 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.

  2. Removing operator++ for bool

    The operator++ for bool is deprecated and was removed in C++17.

    Let's look at the following code:

    C++
    #include <iostream>
    
    int main() {
         bool b1 = false;
         b1++;
    
         return 0;
    }
    

    After compiling the above code, we get the following errors:

    Removing operator++ for bool

    • Error (active) E2788: incrementing a bool value is not allowed
    • Compiler Error C2428: 'operation' : not allowed on operand of type 'bool'
  3. 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):

    C++
    register int x = 99;
    

    We will get the following warning: "warning C5033: 'register' is no longer a supported storage class"

  4. 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:

    C++
    #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:

    Removing std::auto_ptr

    Namespace "std" has no member "auto_ptr"...

  5. 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:

    C++
    #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)"

  6. Removing some deprecated aliases

    std::ios_base::iostate

    std::ios_base::openmode

    std::ios_base::seekdir

  7. Removed allocator from std::function

    The following prototypes were removed in C++17:

    C++
    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" :-)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Canada Canada
Mr.Volynsky Alex is a Software Engineer in a leading software company. Alex is skilled in many areas of computer science. He has over 14 years of experience in the design & development of applications using C/C++/STL, Python, Qt, MFC, DirectShow, JavaScript, VBScript, Bash and of course - C#/.NET.

In addition, Alex is the active member of Intel® Developer Zone (he was awarded by Intel® Green Belt for his active contribution to the Intel Developer Zone community for developers using Intel technology).

Alex is also interested in the Objective-C development for the iPad/iPhone platforms and he is the developer of the free 15-puzzle game on the App Store.

Overall, Alex is very easy to work with. He adapts to new systems and technology while performing complete problem definition research.

His hobbies include yacht racing, photography and reading in multiple genres.
He is also fascinated by attending computer meetings in general, loves traveling, and also takes pleasure in exercising and relaxing with friends.

Visit his C++ 11 blog

Comments and Discussions

 
QuestionMy vote for the article is 5 but I have a comment Pin
_Nizar18-Jul-20 5:45
_Nizar18-Jul-20 5:45 
GeneralThank you for posting this and for your lovely examples for this article! Pin
Member 1477240317-May-20 18:13
Member 1477240317-May-20 18:13 
Bug`string_view` microbenchmark totally wrong Pin
Member 1482749111-May-20 16:42
Member 1482749111-May-20 16:42 
QuestionWhy do you use *outdated* VS2017? Pin
TSchind14-Apr-20 7:42
TSchind14-Apr-20 7:42 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA9-Apr-20 10:23
professionalȘtefan-Mihai MOGA9-Apr-20 10:23 
Questionvote 5 Pin
Member 1478642029-Mar-20 9:41
Member 1478642029-Mar-20 9:41 
SuggestionStructure binding additional sample Pin
Daniele Rota Nodari26-Mar-20 2:48
Daniele Rota Nodari26-Mar-20 2:48 
BugNamespaces samples Pin
Daniele Rota Nodari26-Mar-20 2:39
Daniele Rota Nodari26-Mar-20 2:39 
GeneralMy vote of 5 Pin
koothkeeper16-Mar-20 10:12
professionalkoothkeeper16-Mar-20 10:12 
PraiseExcellent well-thought-out article! Pin
koothkeeper16-Mar-20 10:12
professionalkoothkeeper16-Mar-20 10:12 
GeneralMy vote of 5 Pin
Member 1477240313-Mar-20 13:07
Member 1477240313-Mar-20 13:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.