"Modern" C++ Features

Erich Keane

Software Engineer, Intel C/C++ Compiler Front End

Erich.Keane@intel.com

What is Modern C++?

Plus Presentation Basics:

 

  • For the purposes of this presentation, C++11 and newer
  • This presentation covers up to the DIS for C++17
  • A number of things have been greatly simplified/glossed over.  Treat this presentation as a starting point on 'what to google', not an authoritative source

C++11

nullptr

Typesafe version of "NULL".  Implicitly converts to ANY pointer type, but not to a number.

void g(int* ptr);
void f(int val); // #1
void f(int* ptr); // #2

// NULL is often a cast to a void*, so it can convert to ints:
#define NULL ((void*)0)

g(NULL); 
g(nullptr); // Same meaning!

f(nullptr); // calls #2!
f(NULL); // Calls #1!

// Note: many implementations now do:
#define NULL nullptr

Move

Alternative for "copy" that allows you to 'steal' contents of a temporary:

class X {
  char* myData;
  int dataLen;
  public:
  // Copy CTOR
  X(const X& other):dataLen(other.dataLen) {
    myData = malloc(dataLen);
    memcpy(myData, other.myData, dataLen);
  }
   
  // Move CTOR, language guarantees "other" is a temporary,
  // thus no longer needs its data anymore, so we steal it!
  X(X&& other) : dataLen(other.DataLen) {
    myData = other.myData;
    other.myData = nullptr;
  }
};

Auto Type Deduction

At compile time, the language can "figure out" types declared with "auto".

auto x = 7; // exactly the same as "int x = 7;"

// "p" deduced to return type of begin
for (auto p = v.begin(); p != v.end(); ++p){} 

// Useful when you don't have a good way of knowing the type!
template<typename T, typename V>
void foo(T t, V v) {
    auto value = t + v; // How would you name "value" ?
    ...
}

Range-for

A new form for "for" loops over things with iterators.

// Loops over "v", making "x" a copy of the dereferenced iterator.
for (auto x : v)
    ...

// Or, save yourself a copy(or allow editing inside the loop)
// by making 'x' a reference.
for (auto &x: v)
    ...

Right Angle Brackets

Collections of collections used to not parse right, its now fixed!

list<vector<string>> lvs;
                   ^
// What is the >> above?  Used to be ambiguous,
// since it could be shift. All the compilers "knew" 
// anyway what you were doing, so you can type them
// next to each other.

// Standards compliance used to require:
list<vector<string> > lvs;

Default/Deleted functions

Function bodies can be replaced with a simple alternative:

class X {
  // Copy operator isn't an allowable function call!
  X& operator=(const X&) = delete;

  // Can also delete normal functions, makes them unavailable for lookup:
  void foo(long long){...} // can call with a long long!
  // Disallows this overload, also prevents conversion of long long to short.
  void foo(short) = delete;

  // The destructor's body is exactly what the compiler would generate
  // if you hadn't said anything.  Useful when creating a constructor 
  // would stop auto generation of a copy/move/etc.
  ~X() = default; 
  // Works for default ctor, dtor, copy/move/etc.
  X(const X&) = default;
};

Enum Class

Strongly typed and fully scoped enum!

enum class Color {red, blue};

Color c= 7; // error, doesn't convert to an int!
int i = Color::red; // Error, doesn't convert to int!
// Also note the fully scoped nature!

Color c2 = blue; // illegal, 'blue' needs to be scoped.

// Allows forward decl!
enum class Animal;
...
enum class Animal { lion, tiger, bear}; // Oh my!

constexpr

constexpr Variables are calculated at compile time!

constexpr functions follow specific rules that allow them to generate constexpr variables (normally executed otherwise).

// Function can be calculated compiletime OR runtime
// Is compiletime if all of its parameters are constexpr
constexpr int Foo(int a, int b) {...}

void bar(int i, int j) {
  // Calculated at compiletime, no execution time cost! 
  constexpr int B = Foo(5,3);

  // Legal, calculated at runtime
  int C = Foo(i,j);

  // illegal, can't be calculated at compiletime!
  constexpr int D = Foo(i,j);

  // OK, uses only constexpr parameters
  constexpr int E = Foo(B, 5);
}

Lambda Expressions

Creates an closure (aka, unnamed function, many others).

// Syntax (incomplete):
// [capture list] (parameters) -> ret_type {body}
// Parameters, and ret_type are optional in some cases.

// temp is a function-type
auto temp = [](int a, int b)->int { return a + b; };
// That you can call!
int value = temp(5,4);
int something_else = 5;

// something_else captured by 'copy', value is captured by reference.
auto captured= [something_else, value&]() {value = value = something_else};
captured();

// "this" captures (*this) by reference
auto p = [this](){this->foo = 5;}; // allowed

// [&] captures all automatic variables used by reference.
// [=] captures all automatic variables used by copy.

decltype

Produces the 'declared type' of a variable or expression in a 'type' context:

struct A { double x; };
const A* a = new A{0};
 
decltype(a->x) y;       // type of y is double (declared type)

template <typename T>
void Foo(T t) {
  // Both are the same type!
  auto temp = t.begin();
  decltype(temp) other_temp = temp;

  auto f = [](int a, int b) -> int { return a * b;};
  decltype(f) g = f; // 'g' is the otherwise unnameable type of the lambda.
}

Braced initialization

Alternate constructor syntax, makes things a bit less ambiguous.  Makes construction like array initialization.

Foo f (5,7); // calls a ctor with 2 ints.
Foo f2 {5,7}; // Does the same thing.

Foo f3 {}; // Default constructs f3
Foo f4(); // Most vexing parse :)
int i{}; // Initializes to 0, otherwise would be uninitialized.

// TONS more rules here, please look this up!

initializer_list

"Other" types can take array-like construction by creating an initializer_list ctor.

vector::vector(std::initializer_list<T> s){ // list of T ctor
  //'s' can be iterated, or checked for size, nothing else
  // ie: has begin,end,size, no indexing!
  ...
}
vector::vector(size_type count, const T& value = T()){...}

vector<int> v {1,2,3,4}; // creates a vector with these elements.

// Note that braced initialization prefers non-initializer-list
vector<int> v2 {7}; // calls 'count' CTOR. v2 is length 7.
vector<int> v3 {7,5}; // calls 'count', v3 is length 7, all values are 5.

vector<int> v4 {{7,6}};// forces initializer_list ctor

delegating constructors

Constructors can finally call other constructors!

class X {
    int a;
    // Default ctor calls 'int' ctor with value 42
    X() : X(42) {}
    X(int x) : a(x) {}
};

static_assert

Compile-time assertions

template<typename T>
void foo(T t) {
    static_assert(sizeof(long)>=8, "Long must be 8 bytes for this library to work!");
    static_assert(is_integral<T>::value, "Parameter must be an integral type!");
}
// NOTE: C++17 allows omitting 2nd parameter.

Suffix Return Type

Similar to lambdas, return type can be put 'after' the function parameters:

template<typename T, typename U>
auto mul(T t, U u) ->decltype(t + u)
{ return x * y; }

auto add(int a, int b) -> int { return a + b;}

Template alias

Better alternative to typedef!

using IntVec = std::vector<int>; // more clear as to what it does!

typedef void (*PFD)(double); // PFD is a pointer to a void(double) function, yucky!
using PF = void(*)(double);  //more readable!

// Can be templated!
template<typename T>
using Collection = vector<list<T*>>;

Collection<int> c;
Collection<float> cf;

Variadic Templates

Allows variadic functions to be implemented in a typesafe manner!

template<typename T, typename... Args>		// note the "..."
void printf(const char* s, T value, Args... args)	// note the "..."
{
  while (s && *s) {
    if (*s=='%' && *++s!='%') {	// a format specifier (ignore which one it is)
	std::cout << value;		// use first non-format argument
	return printf(++s, args...); 	// ``peel off'' first argument
    }
    std::cout << *s++;
    }
  throw std::runtime error("extra arguments provided to printf");
}

User Defined Literals

Allows users to create user versions of "1.2F" type syntax:

// A good example:
auto t = "Hi!"s; // is a std::string
// User defined ones (non library) must begin with a "_"
struct Distance {
 //...
 void operator"" _km(long double);
 void operator"" _mi(long double);
};

auto x = 5.5_km; // creates a "Distance" object in a user defined manner.
auto y = 5.5_mi; // creates a "Distance" object in a user defined manner.

// Allowable signatures:
( const char * )
( unsigned long long int )
( long double ) 	
( char )
( wchar_t )
( char16_t )
( char32_t )
( const char * , std::size_t )
( const wchar_t * , std::size_t )
( const char16_t * , std::size_t )
( const char32_t * , std::size_t )

Attributes

Standards version of __attribute__/__declspec

Very few builtin, but compilers extend.

void f [[noreturn]]();
void g [[gnu::force_inline]](); // compiler extension

noexcept

Declare a function as non-throwing!

void foo() noexcept; // I declare this function doesn't throw!
// note, if my implementation of foo were to throw anyway, 
// std::terminate would be called.

// Conditional noexcept:
template<typename T>
void bar(T& coll) noexcept(v.at(0)){ // Can only throw if v.at(0) does
  for (int i; i < coll.size(); ++i) v.at(i) = "bar";
}

override/final

Better errors for virtual function call overriding.

struct Base {
  virtual void f();
  virtual void g() const;
  virtual void h(char);
  void k(); // not virtual!
  virtual void m();
};
struct D : Base {
  void f(); // properly overrides
  void f() override; // same

  void g(); // surprisingly, doesn't override
  void g() override; // error, wrong type!

  virtual void h(char); // overrides
  
  void k() override; // error, nothing to override!
  
  virtual void m() final; // cannot override, error to try.
};

std::array

A better/free 'array' implementation:

// Acts like a C array, but can be properly passed to functions with size!
std::array<int, 7> a; 
int[] b;

void foo(int[] b) { cout << sizeof(b);} // always prints sizeof(ptr)!

void bar(std::array<int, 7>& a); // pass by reference, a.size is 7!

Smart pointers

RAII container replacements for pointers:

// A type that doesn't allow copying, so it is the ONLY owner!
// Can be 'returned', since that causes a temporary, thus 
// gets moved from.
// Guarantees it is destroyed when it goes out of scope!
std::unique_ptr<Foo> f = make_unique<Foo>(1,2,3);

// A type that reference-counts, last one out deletes the object!
std::shared_ptr<Foo> s = make_shared<Foo>(1,2,3);

// A 'weak' pointer, can only be acquired from a shared_ptr,
// and can only be used to convert to a shared_ptr:
std::weak_ptr<Foo> weak = s;
// .. more stuff, 's' may have deleted its things!

// Lock returns nullptr if the shared_ptr no longer exists.
if (shared_ptr<Foo> temp = weak.lock()) {
  // Do stuff with 'temp'
} else {
 // The foo has been destroyed already!
}

Other useful types:

std::thread  // RAII container for threads!

// RAII contains for mutexes/CVs
std::mutex 
std::timed_mutex
std::recursive_mutex
std::condition_variable

// RAII containers for locking/unlocking mutexes!
std::unique_lock
std::lock_guard // same as above, allows ownership transfer.

// Tons of time handling stuff
<chrono>, includes clocks, time_point, conversions, etc.

// Templated, creates atomic versions of 
std::atomic

// "simpler" task spawning
std::future/std::shared_future/std::atomic_future
std::promise
std::packaged_task
std::async

<random> // Safer/better RNG, allows distribution and engine selection.

std::regex // Regular expressions!

C++14

C++14 in a nutshell:

Relaxed rules for a large number of C++11 features, plus a few others!

//Return type deduction: Trailing return types can be omitted in some cases.
auto square(int n) {return n + n;}
//Constexpr expanded: Now much of the language is allowed 
// in "constexpr" functions (not just return statements!).

// Generic lambdas!
auto mult = [] (auto a, auto b) { return a * b; };
// Lambda named captures:
// 'p' is the moved from 'p', 'q' is the name for a 
// copy of 'bar' in the scope of the lambda.
auto lambda = [p = move(p), q = bar]{};

// constexpr variable templates!
template <typename T>
constexpr T pi = T(1415926535897932385);

// attribute deprecation!
class [[deprecated]] old {};
[[deprecated]] void foo();

// Binary Literals:
int val = 0b1234;
// Digit Separators:
float one_million = 1'000'000.00;

C++14 builtin User Defined Literals:

// standard user-defined literals:
//"s" for string:
auto str = "hello world"s;
// "h", "min", "s", "ms", "us", "ns" for std::chrono::duration:
auto dur = 1230s;
// "if", "i", "il" for std::complex<float>,std::complex<double>,
// std::complex<long double>
auto cplx = 1i;

C++17

Some Removals:

Things that are deprecated or entirely removed:

  • Trigraphs removed
  • 'register' keyword now meaningless/semantic-less
  • auto_ptr, random_shuffle, old parts of <functional> gone!

C++17, minor changes:

A few small, but useful changes.

// Variadic template fold-expressions:
template <typename ...Args> auto f(Args ...args) {
return (0 + ... + args); }
// Template auto for non-type parameters:
template <auto Size> struct S;

// Class template argument deduction, CTORs now deduced!
pair p(1,'x'); instead of pair<int, char> p(...);

// Inline variables.  The following has all definitions refer to the same entity:
inline int n = 10;

// constexpr lambdas:
[&](auto x) constexpr {return x + 5;};

constexpr if:

A compile-time 'if', only works on constexpr/compiletime constants:

template<typename T>
auto get_thing(T t) {
    if constexpr(is_pointer_v<T>)
        return *t;
    else if constexpr(is_floating_point_v<T>)
        return static_cast<int>(t);
    else
        return t;
}

structured bindings:

An anonymous, transparent object!

// Allows decomposition of a std::tuple, struct, or other type!

struct Foo {int a; int b; double c; string d;};
std::tuple<int, double, float> get_tup();
Foo get_foo();

[i, d, f] = get_tup();
[f_a, f_b, f_c, f_d] = get_foo();

// All above names introduced into current scope!
// structured bindings can have c-v qualifications,
// as well as be a reference!

C++17 Library changes:

Important library changes to C++17.

std::string_view // pointer/length pair that emulates a std::string
string_view s = some_string; // observation pointer!
<filesystem> // based on boost::filesystem, a ton of libraries for FS stuff

// <algorithm> extended to allow execution policies for parallelized algorithms.
// does a find, but allows it to be executed in parallel where sequence
// doesn't matter.
std::find(par_unseq, v.begin(), v.end(), what_to_find());

//std::optional, a type wrapper to contain a possibly-valid value, checked
// via optional::value()/optional::value_or()
std::optional<std::string> create(b) {
    if (b) return "Godzilla";
    return {}; // returns an 'empty' optional.
}
void use() {
  std::cout << create(true).value_or("empty") << '\n';
  if (create(false))
    std::cout <<"Empty!\n";
}

C++17 Library changes:

Important library changes to C++17, continued.

// std::variant is a type-safe union, internally contains a 'which'/value pair.
std::variant<int, float, int, double, string> v; // can contain any of those.
// accessed via std::get<integral> or std::get<type>, throws if type/int is wrong:
v = "str";
try { std::get<0>(v);} catch(std::bad_variant_access&){}
// can get which one by variant::index() (returns a size_t);

// std::any contains a single value of ANY type.  Provides 'has_value' (returns bool)
// and 'type' (returns typeid) methods.  any_cast used to get a value out.

Q/A

Erich.Keane@intel.com

Modern C++ features

By Erich Keane

Modern C++ features

  • 991