Home C++ Introduction Decisions Loops Input/Output Functions Stack and Heap References Arrays Searching and Sorting Recursion Pointers Character and Strings Structures Classes Inheritance Exceptions Templatess STL Modern C++ Misc Books ----

C++ 17 New Features


Contents

Template Class Deduction

We can deduce types for function templates. We can also deduce template types for classes with C++ 17.

File: class1.cpp
#include <iostream>
#include <thread>
#include <mutex>

using namespace std ;

mutex mtx ;

void thread_function1()
{
   for( int i1=0 ; i1<5 ; i1++ )
     {
        // Pre C++ 11
       //lock_guard<mutex> lk( mtx ) ;
       //With C++ 17
       lock_guard lk( mtx ) ;
       cout << "Thread function1: " << i1 << endl ;

     }

}



int main()
{
    thread t1( &thread_function1 );   // t1 starts running
    //cout << "main thread\n";

    // main thread waits for the thread t1 to finish
    t1.join();



    return 0;
}

Folding Expression

We had variadic templates that could handle a different number of arguments of different types. However the implementation was recursive. We have "folding expressions" in C++ 17 that gets rid of the recursive approach.

File: folding1.cpp
// C++ program to illustrate unary leftfold expression
#include <iostream>
using namespace std;

//Unary Left Fold:
//Syntax: (... op pack)
//Expansion: (((E1 op E2) op ...) op EN)
// sum_right(1, 2, 3) expands to ((1 + 2) + 3))

template <typename... Args> auto sum_left_fold_unary(Args... args)
{
    return (... + args);
}

//Unary Right Fold
// (  1 + ( 2 + 3 ) )
 template <typename... Args>
    auto sum_right_fold_unary(Args... args) {
        return (args + ...); // Unary Right Fold with addition
    }

// (((value + 3 ) + 1 ) + 2 )
//sum_left_fold_binary(  3, 1, 2 )
template <typename T,  typename... Args> auto sum_left_fold_binary(
    T initial_value ,  Args... args  )
{
    return (initial_value + ... + args);
}

// (((value + 3 ) + 1 ) + 2 )
//sum_left_fold_binary(  3, 1, 2 )
// ( value + (3 + (1 + 2)  ) )
template <typename T,  typename... Args> auto sum_right_fold_binary(
    T initial_value ,  Args... args  )
{
    return (initial_value + ... + args);
}

// Function template that accepts a variadic number of arguments
template<typename... Args>
void print_to_cout(Args&&... args)
{
    //binary operator
    // This expands to (cout << arg1 << arg2 << ... << argN)
    ( cout << ... << forward<Args>(args)  );

    cout << '\n'; // Add a newline at the end
}


int main()
{
    int sum1 = sum_left_fold_unary(  3, 1, 2 )  ;
    cout << "Result: " << sum1 << endl;

    int sum2 = sum_right_fold_unary(  3, 1, 2 )  ;
    cout << "Result: " << sum2 << endl;

    int sum3 = sum_left_fold_binary(  3, 1, 2 )  ;
    cout << "Result: " << sum3 << endl;

    int sum4 = sum_right_fold_binary(  3, 1, 2 )  ;
    cout << "Result: " << sum4 << endl;


    print_to_cout("Hello", " ", "World", "!")  ;
    print_to_cout(1, 2.5, 'c', "string_literal")  ;

    return 0;
}


File: folding2.cpp
#include <iostream>
#include <string>


using namespace std ;

template <typename T>
void Log(T object) {
    std::cout << object << ' ';
}

template <typename... Types>
void Fold(Types... args)
{
    // Unary right fold with the comma operator
    (Log(args), ...);
    // Expands to: Log(arg1), Log(arg2), Log(arg3), ...
}

int main()
{
    Fold("Hello", "World", 123, 4.5); // Output: Hello World 123 4.5

    return 0 ;
}

Attributes

C++ 17 has introduced new attributes namely: [[fallthrough]], [[nodiscard]], [[maybe_unused]] . These attributes are used so that the compiler does not generate the warnings It's possible that your compiler does not generate the warnings if these attributes are missing. Check your compiler options.

File: attribute1.cpp
#include <iostream>
#include <thread>
#include <mutex>

using namespace std ;

[[nodiscard]]
bool function1()
{
  return true;
}



int main()
{
    int x1 = 4 ;
    switch (x1)
    {
      case 1:
        cout << "1" << endl ;
        [[fallthrough]];
      case 2:
        cout << "2" << endl ;
        break;
      case 3:
        cout << "3" << endl ;
        [[fallthrough]];
      default:
        cout << "4" << endl ;
    }

  //return value not used
  function1() ;


 [[maybe_unused]]
  bool error ;


    return 0;
}
[[fallthrough]] Used in a switch statement. Indicates that there is no
                break and control falls through to the next opeion.
[[nodiscard]] The function returns a value but the return value is never used.

[[maybe_unused]] A variable is declared but never used.

Capture this

Pre C++ 17 the "this" could be captured by only. This could lead to issues in call back related scenarios if the lambda was called after the object was out of scope.

File: capture1.cpp
#include <iostream>
#include <thread>
#include <mutex>

using namespace std ;

class A
{
  public:
  int value {123};
  auto getValueCopy()
  {
    return [*this] { return value; };
  }
  auto getValueRef()
  {
    return [this] { return value; };
  }


};






int main()
{
    A obj1 ;
    //valueCopy and valueRef are lambdas
    auto valueCopy = obj1.getValueCopy() ;
    auto valueRef = obj1.getValueRef() ;
    obj1.value = 321;
    cout << "valueCopy:" << valueCopy() << endl  ;
    cout << "valueRef:" << valueRef() << endl  ;







    return 0;
}
Output
$ g++ capture1.cpp ; ./a.exe
valueCopy:123
valueRef:321


From the above file:

  auto getValueCopy()
  {
    return [*this] { return value; };
  }
  auto getValueRef()
  {
    return [this] { return value; };
  }

The "getValueCopy()" captures "this" by value and thus
has a copy of the object. The "getValueRef()" copies the
"this" by reference. Actually it gets the pointer copy to the
original object which is sort of the same thing as a reference.


	auto valueCopy = obj1.getValueCopy() ;
	auto valueRef = obj1.getValueRef() ;
	obj1.value = 321;
	cout << "valueCopy:" << valueCopy() << endl  ;
	cout << "valueRef:" << valueRef() << endl  ;

We store the lambdas in "valueCopy" and "valueRef".
We then modify the data member "value" in the original
object. From the output we can see that "valueCopy()"
returned the unchanged value because it had its own
copy while the "valueRef()" returns the new modified value.

Inline Variables

We have the concept of inline functions and now we can have inline variables. We can have multiple definitions in several different files as the below programs show. Only one of the definitions will be picked.

File: inline1.cpp
#include <iostream>
using namespace std ;

inline int x1 ;

class A
{
    public:
      A() : id{count++}
      {

      }
      ~A() { }
      int id  ;
      static inline int count{0} ;
      // declare and initialize count to 0 within the class
      //Pre C++ 17
      static  int count1 ;
};

int A::count1 = 0 ;

int main()
{
   A obj1 ;
   cout << "obj1.id:" <<  obj1.id << endl ;
   A obj2 ;
   cout << "obj2.id:" <<  obj2.id << endl ;

}


File: inline2.cpp
inline int x1 ;
We have defined
inline int x1 ;
in both the files and do not get a compiler error when
compiling the files.

We also have one other use case for inline variables.

We can now define a static variable in a class with inline
and then do not have to define it outside the class.

class A
{
	public:
	  A() : id{count++}
	  {

	  }
	  ~A() { }
	  int id  ;
	  static inline int count{0} ;
	  // declare and initialize count to 0 within the class
	  //Pre C++ 17
	  static  int count1 ;
};

int A::count1 = 0 ;

The "count1" shows how we would define pre C++ 17 inline
qualifier while "count" is defined with inline without
the need to define it outside the class .

Namespaces

C++17 gives us a more compact way of writing the syntax for nested naespaces.

File: namespace1.cpp
#include <iostream>
#include <thread>
#include <mutex>

using namespace std ;

//Pre C++ 17
namespace A {
  namespace B {
    namespace C {
      int x1;
    }
  }
}

namespace A::B::C
{
    int x2 ;
}

int main()
{
   A::B::C::x1 = 10 ;
   A::B::C::x2 = 11 ;


    return 1 ;
}

Variant

A variant is a like a union that provides type safety.

File: variant11.cpp
#include <iostream>
#include <thread>
#include <variant>

using namespace std ;


int main()
{
    variant<int, double> v{ 12 };
    cout << get<int>(v)  << endl    ; // == 12
    cout << get<0>(v)  << endl    ; // == 12
    v = 12.0;
    cout << get<double>(v) << endl  ; // == 12.0
    cout << get<1>(v)  << endl ; // == 12.0

    //Compiler error 'std::bad_variant_access' exception
    //cout << get<0>(v)  << endl    ; // == 12

    return 1 ;
}

Optional

The "Optional" is a template class that may contain a valid value or not. Usually a sentinel like a null pointer or zero value or some other mechanism is needed to state that a valid value does not exist. The "Optional" provides a more standard way to provide similar functionality.

File: optional1.cpp
#include <iostream>
#include <optional>
#include <string>
#include <map>

using namespace std ;


// username with id
map<string, int> students_db =
{
    {"John Kerr", 12501 },
    {"Fred Louis", 32502 },
    {"David Marlowe", 42503 }
};


optional<int> get_student_id(  const string& name )
{
    auto it = students_db.find( name )   ;
    if (  it != students_db.end())
    {
        return optional<int>(it->second)  ;
    }
    return nullopt; // Explicitly returns "nothing"
}

optional<int> get_student_id1(  const string& name )
{
    auto it = students_db.find( name )   ;
    if (  it != students_db.end())
    {
        return it->second  ;
    }
    return {} ; // Explicitly returns "nothing"
}





int main()
{

    string student_name  = "John Kerr" ;
    optional<int> id = get_student_id( student_name )  ;

    // 1. Check if the optional contains a value using contextual bool conversion
    //optional can be used as a condition and will produce true if
    //it contains a valid value
    if ( id )
    {
        // 2. Access the value safely using the * operator
        cout << student_name << "'s id is: " << *id << endl;
    }
    else
    {
        cout << student_name << " was not found." << endl;
    }

    //Another way
    if ( id.has_value() )
    {
        // 2. Access the value safely using the * operator
        cout << student_name << "'s id is : " << id.value() << endl;
    }
    else
    {
        cout << student_name << " was not found." << endl;
    }

     student_name  = "Charles Bronson" ;
     //Use the second function.
     id = get_student_id1( student_name )  ;

     cout << student_name << " id is: " <<
      id.value_or(0) << endl;



}
This is the function declaration. Here we state that
"optional"  object is returned. This object may contain
value of type int or may not.

optional get_student_id(  const string& name )

Inside the function we return a valid value with the statement:

optional(it->second)

The "it->second" is the value for the key in the map. If the
key could not be found then we return "nullopt" . This states that
the optional object does not contain a value.

The second version of the function :

optional get_student_id1(  const string& name )

shows some alternate methods of returning values.

    if (  it != students_db.end())
    {
		return it->second  ;
    }
    return {} ; // Explicitly returns "nothing"

We can return just the value with
		return it->second  ;
and an implicit cast takes place.

And with "return {}" a "nullopt" is returned. The "nullopt" is a constant
with no values while "{}" is a default initialization

We can directly use an "any" object in a if or while condition.
If the "any" object has a vaild value then the bool condition
returns true else returns false.

    if ( id )
    {
        // 2. Access the value safely using the * operator
        cout << student_name << "'s id is: " << *id << endl;
    }
    else
    {
        cout << student_name << " was not found." << endl;
    }

Another way to check is using the expression "anyObject.has_value()" .

if ( id.has_value() )

The expression "id.value_or(0)" will return the valid value or the value we
give in the argument.

any

The "any" is a container that can hold a single valueof any type. So what's so special about that. A tuple with a single element can also hold a single element.
The same "any" object can also hold a value of another type later on in the program.

File: any1.cpp
#include <iostream>
#include <vector>
#include <any>
#include <string>

using namespace std ;

int main()
{

  any x1 {5};
  cout << x1.has_value() << endl ; // == true
  cout << any_cast<int>(x1) << endl ;  // == 5

   if (x1.type() == typeid(int) )
     {
              // Extract using any_cast
              int value = any_cast<int>(x1);
              cout << "Found an integer: " << value << "\n";
     }

   //The type can be changed dynamically
   x1 = string{ "Some string" }  ;
   cout << any_cast<string>(x1) << endl ;


}
  any x1 {5};
  cout << x1.has_value() << endl ; // == true
  cout << any_cast(x1) << endl ;  // == 5

We create an "any" object by simply defining the variable
without specifying the type. The type is deduced and saved
( not directly but maybe in something like a function pointer).
The "has_value()" lets us check if the value exists for the any object.
To get the value out we need to specify the type. Now the question
is since it knows the type why do we need to specify it. This is an
internal design decision that was made to avoid ambiguities in case of
casting to different types.

The next example shows how we can have a vector of "any" objects. We can then store objects ( both primitive and class type ) into the vector and get them out. C++ does not have a single root class hierarchy like Java so we cannot use that approach to store different objects of different types. We can't use tuple because then we run into the problem of defining the type for the vector. vector< tuple > dynamic_registry ;

File: any2.cpp
#include <iostream>
#include <vector>
#include <any>
#include <string>


class Student
{
    pubic:
     string name ;
     int id ;
};

int main() {
    // 1. A vector of any can hold single elements of ANY type dynamically.
    // A tuple cannot do this because a tuple has a fixed, strict compile-time type schema.
    vector<any> dynamic_registry ;

    dynamic_registry.push_back(42);                      // Stores an int
    dynamic_registry.push_back(string("Hello C++")); // Stores a string
    dynamic_registry.push_back(3.14159);                  // Stores a double
    dynamic_registry.push_back(Student{"Alice", 101});       // Stores a custom struct

    cout << "--- Processing Dynamic Types via any ---\n";

    // 2. We inspect and extract the types at runtime
    for (const auto& item : dynamic_registry)
    {

        if (item.type() == typeid(int)) {
            // Extract using any_cast
            int value = any_cast<int>(item);
            cout << "Found an integer: " << value << "\n";
        }
        else if (item.type() == typeid(string)) {
            const auto& value = any_cast<const string&>(item);
            cout << "Found a string: " << value << "\n";
        }
        else if (item.type() == typeid(double)) {
            double value = any_cast<double>(item);
            cout << "Found a double: " << value << "\n";
        }
        else if (item.type() == typeid(Student)) {
            const auto& Student = any_cast<const Student&>(item);
            cout << "Found a custom Student struct: [Name: " << Student.name << ", ID: " << Student.id << "]\n";
        }
        else {
            cout << "Unknown type found.\n";
        }
    }


    return 0;
}

std::string_view

This class is a read only class that hold a pointer to an already allocated string. It is known as a lightweight, non-owning sequence of characters. It is useful when we want to pass a "string" object or a C style string to a function that will not modify the original string. It has helper member functions like "substr" that shift internal pointers to generate the string rather than allocating any memory. So it's an efficient way to handle strings. Below program illustrates the usage.

File: stringview1.cpp.cpp

invoke

The "invoke" function can call a function object like a lambda or a regular function later on by saving the function somewhere and then later invoking it. This can be used to write generic code that can work with different functions. It's use case is slightly different from "std::function" . The "std::function" is a class template that can hold a certain kind of function ( with arguments and return values ) while the "invoke" is a utility function that can take any kind of function and execute it.

File: invoke1.cpp
The below file shows how "invoke" can execute a member function of a class.

File: invoke2.cpp

apply

The "apply" is similar to "invoke" but works with a tuple that supply the arguments.

File: apply1.cpp

filesystem

This package provides an agnostic way to manipulate file system operations such as getting sizes, creating folders across different operating systems.

File: file1.cpp

byte

The "byte" is a new type. It differes from using "char" or "unsigned char" as it does not allow arithmetic operations and only allow bitwise operations.

File: byte1.cpp

maps and sets

Certain methods have been added to these classes that make it efficient to take out nodes and insert nodes.

File: mapset1.cpp

	std::map src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
	std::map dst {{3, "three"}};
	dst.insert(src.extract(src.find(1))); // Cheap remove and insert of { 1, "one" } from `src` to `dst`.
    printMap( src ) ;
    printMap( dst ) ;

The new operation "extract" takes out the node from the source. However it
does not copy the node or allocate new memroy but rather manipulates
the internal pointers used in the data structure of map.

It then inserts it into the destination map.

	std::set src_set {1, 3, 5};
	std::set dst_set {2, 4, 5};
	dst_set.merge( src_set  )  ;
	printSet( dst_set )  ;

The "src_set" elements are inserted into the "dst_set" without
copying the elements.

	std::map map1 {{1, "one"}, {2, "two"}, {3, "three"}};
	auto node = map1.extract(2);
	node.key() = 4;
	map1.insert( std::move(node) );
    printMap(  map1 ) ;

We remove an element from the map and then change the key and insert it
back again.

parallel

We can specify different execution policies for certain algorithms such as: copy, find, sort.
There are 3 policies:
 seq sequential
 par parallel
 par_unseq parall unsequenced which can make use of hardware features
 such as SIMD ( Single Instruction Multiple Data ) .


File: par1.cpp

sample

This function can pick out random number of elements from a container.

File: sample1.cpp


clamp

This function takes a value, min and max. If the value is below min then min is returned. If the value is greater than max then max is returned and if the value in between then the value is returned.

File: clamp1.cpp


reduce

This function can apply a function to a range of elements and store the result into a single value. The default function is std::plus with the result initial value of 0. It's able to operate in parallel and is different from "std::accumulate" . It can also be thought of as a folding operation.

File: reduce1.cpp
	const array arr1{ 1, 2, 4 };
	int result = reduce(cbegin(arr1), cend(arr1)); // == 7

This starts off with an initial value of 0 asnd then adds rest of the
elements as:
 ( (( 0 + 1 ) + 2 ) + 4 )

	result = reduce(cbegin(arr1), cend(arr1), 1,
	multiplies<>{}); // == 8
This folds to:

  ( (( 1 * 1 ) * 2 ) * 4 )

prefix

This function can apply a function to a range of elements and store the result into a single value. The default function is std::plus with the result initial value of 0. It's able to operate in parallel and is different from "std::accumulate" . It can also be thought of as a folding operation.

File: prefix1.cpp