New features in C++

Note: taking into account the actual scenarios in OI, this article will not fully study the grammar, only the part that could be applied.

The syntax of this article refers to the C++11 standard. Those with different semantics will use C++11 as the standard. The syntax of C++14 and C++17 might be mentioned if appropriate and will be specially marked.

auto type specifier

The auto type specifier is used to automatically deduce the types like variables. E.g:

auto a = 1;        // a is integer type
auto b = a + 0.1;  // b is double type

for loop based on range

Below is the syntax of a range-based for loop before C++20:

for (range_declaration : range_expression) loop_statement

The code generated by the above grammar equals to the following code (__range, __begin and __end are for illustration only):

auto&& __range = range_expression;
for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) {
  range_declaration = *__begin;
  loop_statement
}

range_declaration

A range declaration is a declaration of a named variable whose type is the type, or the reference to the type of the element of the sequence represented by the range expression. The auto specifier is usually used for automatic type inference.

range_expression

A range expression is any expression that can represent a suitable sequence (an array, or an object that defines begin and end member functions or free functions), or a list of curly brace initializers. For this reason, we should not modify the range expression in the loop body to invalidate any "iterators" (including "postfix iterators") that have not been traversed.

Here we have an example:

for (int i : {1, 1, 4, 5, 1, 4}) std::cout << i;

loop_statement

The loop statement can be any statement. It is usually a compound statement that is the loop body.

Here we have an example:

#include <iostream>

struct C {
  int a, b, c, d;
  C(int a = 0, int b = 0, int c = 0, int d = 0) : a(a), b(b), c(c), d(d) {}
};

int* begin(C& p) { return &p.a; }
int* end(C& p) { return &p.d + 1; }

int main() {
  C n = C(1, 9, 2, 6);
  for (auto i : n) std::cout << i << " ";
  std::cout << std::endl;
  // the loop below is equivalent to the loop above
  auto&& __range = n;
  for (auto __begin = begin(n), __end = end(n); __begin != __end; ++__begin) {
    auto ind = *__begin;
    std::cout << ind << " ";
  }
  std::cout << std::endl;
  return 0;
}

Lambda expression

Lambda expression is an unnamed function object that can capture variables in the scope. We can understand it as an anonymous inline function. The following is the syntax of Lambda expressions:

[capture] (parameters) mutable -> return-type {statement} 

capture clause

Lambda expressions begin with a capture clause, which specifies which variables are captured and whether the capture is by value or by reference: variables prefixed with the & symbol are accessed by reference, and variables without the prefix are accessed by value. The empty capture clause [] tells the body of the lambda expression not to access the variables in the enclosing scope.

We can also use the default capture mode: & means all captured variables are accessed by reference, and = means all captured variables are accessed by value. We can then specify the opposite mode for a specific variable explicitly.

For example, if the Lambda body wants to access the external variable a by reference and access the external variable b by value, the following clauses are equivalent:

  • [&a, b]
  • [b, &a]
  • [&, b]
  • [b, &]
  • [=, &a]
  • [&a, =]

By default, the variables mentioned in Lambda will be captured.

parameters

In most cases, it is similar to the parameter list of a function, for example:

auto lam = [](int a, int b) { return a + b; };
std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl;

In C++14, if the parameter type is generic, you can use auto to declare the type:

auto lam = [](auto a, auto b)

An example:

int x[] = {5, 1, 7, 6, 1, 4, 2};
std::sort(x, x + 7, [](int a, int b) { return (a > b); });
for (auto i : x) std::cout << i << " ";

This will print the result after sorting the x array descendingly.

mutable

Using variable specifications, the body of the Lambda expression can modify the variables captured by value. If this keyword is used, parameters cannot be omitted (even if it is empty).

return-type

If the body of the Lambda only contains a return statement or does not return a value, this part can be omitted. If the body of the Lambda expression contains a return statement, the return type will be automatically deduced, and the return type will follow the parameters (unless you want to specify one). Otherwise, the compiler will infer the return type as void.

For example, the above lam can also be written as:

auto lam = [](int a, int b) -> int

Here we have two more examples:

auto x1 = [](int i) { return i; };  // OK
auto x2 = [] { return {1, 2}; };    // ERROR: the return type is deduced to void

statement Lambda

The body of the Lambda can contain any part of the function. Both ordinary functions and the body of the lambda expression can access the following variable types:

  • capture variables from enclosed scope
  • parameters
  • locally declared variables
  • when declared in a class, capture this
  • any variable with static storage time, such as global variables

Here we have an example:

#include <iostream>

int main() {
  int m = 0, n = 0;
  [&, n](int a) mutable { m = (++n) + a; }(4);
  std::cout << m << " " << n << std::endl;
  return 0;
}

In the end we get the output 5 0. This is because n is captured by value, and the original value 0 remains unchanged after calling the Lambda expression. The mutable specification allows n to be modified in the main body of the Lambda. If mutable is deleted, the compilation will fail.

decltype

decltype specifiers can infer the type of the expressions.

#include <iostream>
#include <vector>

int main() {
  int a = 1926;
  decltype(a) b = a / 2 - 146;         // b is integer type
  std::vector<decltype(b)> vec = {0};  // vec is std::vector <int> type
  std::cout << a << vec[0] << b << std::endl;
  return 0;
}

constexpr

The constexpr specifier declares that the value of a function or variable that can be obtained at compile time. The main difference between constexpr and const is that constexpr must be initialized at compile time. The constexpr specifier used for object declarations has const, and the constexpr used for function declarations has inline. Let's take a look at an example:

int frac(int x) { return x ? x * frac(x - 1) : 1; }
int main() {
  constexpr int a = frac(5);  // ERROR: function calls must have constant values in constant expressions
  return 0;
}

Add constexpr before int frac(int x) so that compilation would work.

std::tuple

std::tuple is defined in the header file <tuple>, which is a heterogeneous container of fixed size (it cannot be changed after the initial element is determined, but there can be any number of initial elements). It is a promotion of std::pair. Let's take a look at an example:

#include <iostream>
#include <tuple>
#include <vector>

constexpr auto expr = 1 + 1 * 4 - 5 - 1 + 4;

int main() {
  std::vector<int> vec = {1, 9, 2, 6, 0};
  std::tuple<int, int, std::string, std::vector<int> > tup =
      std::make_tuple(817, 114, "514", vec);
  std::cout << std::tuple_size<decltype(tup)>::value << std::endl;

  for (auto i : std::get<expr>(tup)) std::cout << i << " ";
  // std::get<> inside the angle brackets must be an integer constant expression
  // the value of the expr constant is 3. Note that the first element number of std::tuple is 0.
  // so we std::get a std::vector<int>
  return 0;
}

Member functions

Function Usage
operator= assign the contents of one tuple to another
swap swap the contents of two tuple

Example:

constexpr std::tuple<int, int> tup = {1, 2};
std::tuple<int, int> tupA = {2, 3}, tupB;
tupB = tup;
tupB.swap(tupA);

Non-member functions

Function Usage
make_tuple create a tuple object whose type is defined according to the type of each argument
std::get tuple access to specified elements
operator== and more lexicographically compare the values in tuple
std::swap specialized std::swap algorithm

Example:

std::tuple<int, int> tupA = {2, 3}, tupB;
tupB = std::make_tuple(1, 2);
std::swap(tupA, tupB);
std::cout << std::get<1>(tupA) << std::endl;

std::function

The class template std::function is a universal polymorphic function wrapper defined in the header file <functional>. The instance of std::function can store, copy and call any callable (Callable) target-function, Lambda expression or other function objects as well as pointers to member functions and data members.

The stored callable object is called the target of std::function. If std::function does not contain a target, it is empty. Calling an empty std::function target will cause a std::bad_function_call exception to be thrown.

Let's look at an example:

#include <functional>
#include <iostream>

struct Foo {
  Foo(int num) : num_(num) {}
  void print_add(int i) const { std::cout << num_ + i << '\n'; }
  int num_;
};

void print_num(int i) { std::cout << i << '\n'; }

struct PrintNum {
  void operator()(int i) const { std::cout << i << '\n'; }
};

int main() {
  // store free function
  std::function<void(int)> f_display = print_num;
  f_display(-9);

  // store Lambda
  std::function<void()> f_display_42 = []() { print_num(42); };
  f_display_42();

  // store the call to member function
  std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
  const Foo foo(314159);
  f_add_display(foo, 1);
  f_add_display(314159, 1);

  // store the call to data member accessor
  std::function<int(Foo const&)> f_num = &Foo::num_;
  std::cout << "num_: " << f_num(foo) << '\n';

  // store the call to function object
  std::function<void(int)> f_display_obj = PrintNum();
  f_display_obj(18);
}

References

  1. C++ reference
  2. C++ in Visual Studio

评论