Skip to content

New C++ Features

Note: Considering the practical context of competitive programming, this article will not comprehensively study the syntax but will only cover the parts that may be applicable in algorithm competitions.

This article references the C++11 standard for syntax. Where semantics differ, C++11 will be the standard; C++14, C++17, etc., syntax will be mentioned as appropriate and specially noted.

auto Type Specifier

The auto type specifier is used for automatic deduction of the type of variables, etc. For example:

1
2
auto a = 1;        // a is of type int
auto b = a + 0.1;  // b is of type double

Note that auto strips references. If you want to avoid copying overhead, you need to specify it manually:

1
2
3
4
int a = 1;
int& b = a;
auto c = b;   // c is of type int, with copying overhead
auto& e = a;  // e is of type int&, no copying overhead

decltype Specifier

decltype can infer types based on entities or expressions. Note that the two deduce types differently, and incorrect usage may lead to dangling references. It is not commonly used in competitions, so only a brief introduction is given here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>
#include <vector>

int main() {
  int a = 1926;
  decltype(a) b;                 // Deduced from entity, b is of type int
  decltype(1 + 1) c;             // Deduced from expression, c is of type int
  decltype((a)) d = a;           // Deduced from expression, d is of type int&!
  std::vector<decltype(b)> vec;  // Deduced from entity, vec is of type std::vector<int>
  return 0;
}

constexpr

Also refer to Constant Expressions constexpr (C++11)

Range-based for Loop

Use range-based for to traverse iterable objects. Its efficiency is the same as using iterator traversal. Both are generally more efficient than index traversal because they do not require address calculation based on indices.

The following is a simple syntax for range-based for loops:

1
for (item_declaration : range_initializer) statement

For example:

1
2
3
4
std::array<int, 4> arr = {1, 2, 3, 4};
for (int x : arr) {
  std::cout << x << std::endl;
}

The code generated by the above syntax is equivalent to:

1
2
3
4
std::array<int, 4> arr = {1, 2, 3, 4};
for (auto px = arr.begin(), ed = arr.end(); px != ed; ++px) {
  std::cout << *px << std::endl;
}

item-declaration

Declares a variable to receive elements from the container on the right. The variable type must match the element type inside the container. You can use auto for automatic type deduction. For complex types, auto& is often used to avoid copying overhead.

range-initializer

The range initializer can be any iterable object (e.g., an array, or a class object that defines begin and end member functions). If an expression is placed, it will be evaluated only once.

Examples:

1
2
3
4
5
6
7
8
int a[] = {1, 1, 4, 5, 1, 4};
std::vector<int> b{1, 1, 4, 5, 1, 4};
std::map<std::string, int> c{{"114", 114}, {"514", 514}};
for (int i : a) std::cout << i;
for (auto i : b) std::cout << i;
// The type of i below is std::pair<const std::string, int>&
for (auto& i : c) std::cout << i.first << i.second;
for (auto i : {1, 1, 4, 5, 1, 4}) std::cout << i;

Custom Types Supporting Range-based for

You only need to provide begin and end member functions. The return type must support comparison, increment, and dereferencing (the * operator).

Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

struct C {
  int a[4];

  int* begin() { return a; }

  int* end() { return a + 4; }
};

int main() {
  C c = {1, 9, 2, 6};
  for (auto i : c) std::cout << i << " ";
  std::cout << std::endl;
  // output: 1 9 2 6
  return 0;
}

Initialization Statement (C++20)

In C++20, you can also use initialization statements to implement some features, such as loop counters:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <vector>

int main() {
  std::vector<int> v = {0, 1, 2, 3, 4, 5};

  for (int counter = 0; auto i : v)  // the init-statement (C++20)
    std::cout << counter++ << ' ' << i << std::endl;
}

Structured Binding (C++17)

Structured binding is a syntactic sugar provided by C++17, allowing convenient extraction of sub-elements or references to sub-elements, like this:

1
2
3
4
5
6
7
8
struct C {
  int x{1}, y{2};
};

int arr[]{4, 5, 6};

auto [c1, c2] = C{};       // c1=1,c2=2; int type
auto& [a1, a2, a3] = arr;  // a1=arr[0],a2=arr[1],a3=arr[2]; int& type

Note the following points:

  • The number of variables declared on the left must match the number of sub-elements in the object on the right.
  • Type declaration must use auto.
  • You can use & to obtain references.

You can write like this when traversing a map container:

1
2
3
4
5
6
7
8
std::map<std::string, int> m = {{"k1", 1}, {"k2", 2}};

// Using "auto&", no copying overhead
for (auto& [k, v] : m) {
  // k is of type const std::string& because keys are inherently const-qualified
  // v is of type int&
  std::cout << k << ' ' << v << std::endl;
}

std::tuple

Tuple is defined in the header <tuple>. It is a generalization of std::pair and can store multiple values of different types. Let's look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <tuple>
#include <vector>

constexpr auto expr = 4 - 1;  // expr = 3

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);

  // Use get<> to get sub-elements; the angle brackets must contain an integer constant expression
  for (auto i : std::get<expr>(tup)) std::cout << i << " ";
  // The first element is numbered 0, so std::get<3> gives us a std::vector<int>
  return 0;
}

After C++17, you can use structured binding to extract values, like this:

1
2
3
4
5
6
7
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);

auto& [a, b, c, d] = tup;  // C++17 Structured binding
std::cout << a << ' ' << b << c << std::endl;
std::cout << d.size() << ' ' << d[2] << std::endl;

Member Functions

Function Effect
operator= Assigns the content of one tuple to another
swap Swaps the contents of two tuples

Example:

1
2
3
4
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 Effect
make_tuple Creates a tuple object, its type defined by the argument types
std::get Accesses the specified element in a tuple-like way
std::tie Assigns values from a tuple to existing variables
operator== etc. Lexicographically compares values in tuples
std::swap Specialized std::swap algorithm

Example:

1
2
3
4
5
6
7
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;
int x;
std::tie(x, std::ignore) = tupB;
std::cout << x << std::endl;

std::tie assigns tuple elements to existing variables; you can use std::ignore to skip unwanted elements. Structured binding directly declares new variables (supporting value/reference binding) and must accept all elements.

Function Objects

Objects that can use the function call operator operator() are called function objects (FunctionObject).

It is not a language feature but a concept or requirement, widely used in the standard library.

Function objects can be roughly divided into two categories:

  1. Function pointers
  2. Class objects that overload the operator() operator

Lambda is a typical example of the second category. It stores captured content in member variables and overloads the function call operator.

Lambda Expressions

Please refer to the Lambda Expressions page.

std::function

Please Note Performance Overhead

std::function introduces certain performance overhead. According to Benchmark tests, it typically causes 2 to 3 times or more performance loss.

This is because it uses type erasure technology, which is usually implemented via virtual function mechanisms, and calling virtual functions introduces additional overhead.

Consider using Lambda Expressions or Function Objects instead.

std::function is a general-purpose polymorphic function wrapper, defined in the header <functional>.

Instances of std::function can store, copy, and invoke any Callable target, including Lambda Expressions, member function pointers, or other Function Objects.

If std::function contains no callable target (e.g., default-constructed), calling it will throw std::bad_function_call.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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 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 call to data member accessor
  std::function<int(Foo const&)> f_num = &Foo::num_;
  std::cout << "num_: " << f_num(foo) << '\n';

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

Variadic Function Templates

Before C++11, class templates and function templates could only accept a fixed number of template parameters. C++11 allows any number and any type of template parameters.

Here we only briefly introduce variadic function templates.

The following code declares a function template fun that can accept any number and any type of template parameters as its template parameters.

1
2
template <typename... Clazz>
void fun(Clazz... paras) {}

paras is a function parameter pack, accepting 0 or more function arguments. Clazz is a template parameter pack, accepting 0 or more template arguments (non-type, type, or template). When marked with typename, it only accepts types.

It can be simply understood as follows:

  • Template parameter packs are usually some type names (but compile-time constants or template names can also be used).
  • Function parameter packs are usually some variable names.

Now you can call the fun function like this:

1
2
3
4
fun();
fun(1);
fun(1, 2, 3);
fun(1, 0.0, "abc");

Parameter Pack Expansion

Parameter Pack Expansion Syntax

Parameter pack expansion is very simple; use ... and it will automatically be separated by ,. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <class A, class... C>
void func(A arg1, C... arg2) {
  // C is a template parameter pack
  tuple<A, C...>();  // Expands to tuple<int, int, double, bool>();

  // arg2 is a function parameter pack
  func(arg2...);  // Expands to func( 2, 1.1, true );
}

func(1, 2, 1.1, true);

Parameter pack expansion can also include required operations, for example:

1
2
3
4
5
6
7
template <class A, class... C>
void func(A arg1, C... arg2) {
  func((arg2 + 1)...);
  // Expands to func( (2+1) , (1.1+1), (2.1f+1) );
}

func(1, 2, 1.1, 2.1f);

Termination Function

The above function cannot run because the number of parameters keeps decreasing and eventually becomes empty, causing an error.

We need to specify a termination condition. We can provide a regular function, like this:

1
2
3
4
5
6
7
8
9
void func() {}

template <class A, class... C>
void func(A arg1, C... arg2) {
  std::cout << arg1 << std::endl;
  func((arg2 + 1)...);
}

func(1, 2, 1.1, 2.1f);

This way, when the number of parameters is not 0, the template is called; when empty, the regular function is called, and it can run normally.

Fold Expressions (C++17)

C++17 provides a convenient syntax for handling function parameter packs. Its syntax is as follows (must be enclosed in parentheses):

  1. ( pack op ... ) becomes (E1 op (... op (EN-1 op EN)))
  2. ( ... op pack ) becomes (((E1 op E2) op ...) op EN)
  3. ( pack op ... op init ) becomes (E1 op (... op (EN−1 op (EN op I))))
  4. ( init op ... op pack ) becomes ((((I op E1) op E2) op ...) op EN)

A simple demonstration makes it easier to understand:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <class... C>
void func(C... args) {
  (std::cout << ... << args) << std::endl;
  // Syntax 4, equivalent to ↓
  // ( ( ( std::cout << 1 ) << 2.1 ) << true ) << std::endl;
  // Output: 12.11  Note: true outputs as 1 because boolalpha is not specified

  std::cout << (args && ...) << std::endl;
  // Syntax 1, equivalent to ↓
  // std::cout << ( 1 && ( 2.1 && true ) ) ) << std::endl;
  // Output: 1
}

func(1, 2.1, true);

Abbreviated Function Templates (C++20)

Starting from C++20, you can directly use auto ... as parameter types to implement abbreviated function templates:

1
void func(auto... args) { (std::cout << ... << args) << std::endl; }

Note that it is essentially still a function template, equivalent to:

1
2
3
4
template <class... T>
void func(T... args) {
  (std::cout << ... << args) << std::endl;
}

Ranges Library (C++20)

The ranges library is an extension of the iterator and generic algorithm library, making iterators and algorithms more powerful through composition and reducing errors.

A range is a traversable sequence, including arrays, containers, views, etc.

When complex operations on containers or ranges are needed, the ranges library can make algorithm writing easier and clearer.

View

A view is a lightweight object that implements some algorithms through specific mechanisms (such as custom iterators), providing more traversal methods for ranges to meet requirements.

The ranges library has implemented some commonly used views, roughly divided into two types:

  1. Range factories, used to construct some special ranges. Using such factories can eliminate the need to manually construct containers, reduce overhead, and directly generate a range.
  2. Range adaptors, providing various traversal support. They can be called like functions or connected via the pipe operator | to achieve chained calls.

Range adaptors, as range adaptor closure objects, also belong to function objects. They overload operator|, allowing them to be assembled like pipes.

Pipe Operator

The | here should be understood as a pipe operator, not a bitwise OR operator. This usage comes from pipes in Linux.

Even with complex operations, good readability can be maintained, with the following characteristics:

If A, B, C are some range adaptor closure objects, R is some range, and other letters are possible valid parameters, the expression

1
R | A(a) | B(b) | C(c, d)

is equivalent to

1
C(B(A(R, a), b), c, d)

Below is an example using ranges::take_view and ranges::iota_view:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <ranges>

int main() {
  const auto even = [](int i) { return 0 == i % 2; };

  for (int i : std::views::iota(0, 6) | std::views::filter(even))
    std::cout << i << ' ';
}

  1. The range factory std::views::iota(0, 6) generates a range of integer sequences from 0 to 5.
  2. The range adaptor std::views::filter(even) filters the previous range, generating a range containing only even numbers.
  3. The two operations are linked using the pipe operator.

The above code does not require additional heap space to store the generated ranges at each step. The actual generation and filtering operations occur during traversal (more specifically, inside the iterator construction, increment, and dereference), i.e., zero overhead.

At the same time, the lifetime of the external input range is equal to the lifetime of the internal elements of the range adaptor. If the external range (e.g., container, range factory) has been destroyed, then traversing these views again has the same effect as dereferencing a dangling pointer, which is undefined behavior.

To avoid the above situation, you should strictly require that the adaptor's lifetime is within the lifetime of any range it uses.

When the range is destroyed, all elements in the view become dangling
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <ranges>
#include <vector>

using namespace std;

int main() {
  auto view = [] {
    vector<int> vec{1, 2, 3, 4, 5};
    return vec | std::views::filter([](int i) { return 0 == i % 2; });
  }();

  for (int i : view) cout << i << ' ';  // runtime undefined behavior

  return 0;
}

Constrained Algorithm

C++20 provides constrained versions of most algorithms in the namespace std::ranges. They can specify ranges using iterator-sentinel pairs or a single range as arguments, and support projections and pointer-to-member callable objects. Additionally, the return types of most algorithms are changed to return all potentially useful information calculated during algorithm execution.

These algorithms can be understood as improved versions of the old standard library algorithms. They are all function objects, providing more friendly overloads and input parameter type checking (based on concept). Let's first take the comparison between std::sort and ranges::sort as an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;

int main() {
  vector<int> vec{4, 2, 5, 3, 1};

  sort(vec.begin(), vec.end());  // {1, 2, 3, 4, 5}

  for (const int i : vec) cout << i << ", ";
  cout << '\n';

  ranges::sort(vec, ranges::greater{});  // {5, 4, 3, 2, 1}

  for (const int i : vec) cout << i << ", ";

  return 0;
}

ranges::sort and sort have the same algorithm implementation, but ranges::sort provides range-based overloads, making parameter passing more concise. Most other algorithms in the std namespace also have corresponding range overload versions in the ranges namespace.

Using these range parameters combined with the views from the previous section allows us to maintain code readability while performing complex operations. Let's look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <algorithm>
#include <array>
#include <iostream>
#include <ranges>

using namespace std;

int main() {
  const auto& inputs = views::iota(0u, 9u);  // Generate integer sequence from 0 to 8
  const auto& chunks = inputs | views::chunk(3);  // Split the sequence into chunks of 3 elements each
  const auto& cartesian_product =
      views::cartesian_product(chunks, chunks);  // Compute Cartesian product of chunks with themselves

  for (const auto [l_chunk, r_chunk] : cartesian_product)
    // Compute the sum of integers from two chunks under Cartesian product
    cout << ranges::fold_left(l_chunk, 0u, plus{}) +
                ranges::fold_left(r_chunk, 0u, plus{})
         << ' ';
}

Output:

6 15 24 15 24 33 24 33 42

References

  1. C++ Reference