21 new C++ features you will definitely need

So, fate brought you back to C++, and you’re amazed at its capabilities in terms of performance, convenience, and code expressiveness. But here’s the problem: you get lost in this variety of great new features and, as a result, find it difficult to immediately determine which of all of this you really should take into service in your daily work of writing code. Don’t be upset, in this article you will be presented with 21 new features of modern C ++ that will help make your project better and easier to work on.

The C++ community adds to the standard more often than Apple releases new iPhones. This makes C++ more like a big elephant, and it’s impossible to eat a whole elephant in one sitting. That’s why I decided to write this article to give your journey through modern C++ a kind of starting point. My target audience here is people who are migrating from old (i.e. 98/03) C++ to modern (i.e. 2011 onwards) C++.

I have selected a number of modern C++ features and tried to explain them with concise examples so that you can learn to identify the places where they can be used.

Number digit separators

int no = 1'000'000;                      // визуальное разделение единиц, тысяч, миллионов и т.д.
long addr = 0xA000'EFFF;                 // визуальное разделение 32-битного адреса на
uint32_t binary = 0b0001'0010'0111'1111; // удобочитаемые сегменты
  • Previously, you had to count digits or zeros, but starting with C++14, you can make large numbers much more visual.

  • This feature helps to facilitate navigation through words and numbers. Or, let’s say you can increase the readability of a credit card or social security number.

  • Grouped bits will make your code a little more expressive.

Type aliases

template <typename T>
using dyn_arr = std::vector<T>;
dyn_arr<int> nums; // эквивалентно std::vector<int>

using func_ptr = int (*)(int);
  • Semantically similar to using typedefhowever type aliases are easier to read and compatible with C++ templates. Thank C++11.

Custom literals

using ull = unsigned long long;

constexpr ull operator"" _KB(ull no)
{
    return no * 1024;
}

constexpr ull operator"" _MB(ull no)
{
    return no * (1024_KB);
}

cout<<1_KB<<endl;
cout<<5_MB<<endl;
  • For the most part, these will be some real units, such as kb, mb, km, cm, rubles, dollars, euros, etc. User-defined literals allow you not to define functions to perform unit conversion at run time, but to work with it like other primitive types.

  • Very handy for units and measurements.

  • By adding constexpr, you can achieve zero runtime performance impact, which we will see later in this article, and you can read more about it in another article I wrote – “Using const and constexpr in C++“.

Uniform initialization and initialization of non-static members

Previously, you had to initialize fields to their default values ​​in a constructor or in an initialization list. But starting with C++11, you can set regular class member variables (those not declared with the keyword static) initializing a default value, as shown below:

class demo
{
private:
    uint32_t m_var_1 = 0;
    bool m_var_2 = false;
    string m_var_3 = "";
    float m_var_4 = 0.0;

public:
    demo(uint32_t var_1, bool var_2, string var_3, float var_4)
        : m_var_1(var_1),
          m_var_2(var_2),
          m_var_3(var_3),
          m_var_4(var_4) {}
};

demo obj{123, true, "lol", 1.1};
  • This is especially useful when several fields are used as fields at once. nested objectsdefined as shown below:

class computer
{
private:
    cpu_t           m_cpu{2, 3.2_GHz};
    ram_t           m_ram{4_GB, RAM::TYPE::DDR4};
    hard_disk_t     m_ssd{1_TB, HDD::TYPE::SSD};

public:
    // ...
};
class X
{
    const static int m_var = 0;
};

// int X::m_var = 0; // не требуется для статических константных полей

std::initializer_list

std::pair<int, int> p = {1, 2};
std::tuple<int, int> t = {1, 2};
std::vector<int> v = {1, 2, 3, 4, 5};
std::set<int> s = {1, 2, 3, 4, 5};
std::list<int> l = {1, 2, 3, 4, 5};
std::deque<int> d = {1, 2, 3, 4, 5};

std::array<int, 5> a = {1, 2, 3, 4, 5};

// Не работает для адаптеров
// std::stack<int> s = {1, 2, 3, 4, 5};
// std::queue<int> q = {1, 2, 3, 4, 5};
// std::priority_queue<int> pq = {1, 2, 3, 4, 5};
  • Assign values ​​to containers directly with an initializer list, as you can with C arrays.

  • This is also true for nested containers. Say thanks to C++11.

auto&decltype

auto a = 3.14; // double
auto b = 1; // int
auto& c = b; // int&
auto g = new auto(123); // int*
auto x; // error -- `x` requires initializer
  • auto-typed variables are inferred by the compiler based on the type of their initializer.

  • Extremely useful in terms of readability, especially for complex types:

// std::vector<int>::const_iterator cit = v.cbegin();
auto cit = v.cbegin(); // альтернатива

// std::shared_ptr<vector<uint32_t>> demo_ptr(new vector<uint32_t>(0);
auto demo_ptr = make_shared<vector<uint32_t>>(0); // альтернатива
  • Functions can also infer the return type with auto. In C++11, the return type must be specified either explicitly or with decltypeFor example:

template <typename X, typename Y>
auto add(X x, Y y) -> decltype(x + y)
{
    return x + y;
}
add(1, 2);     // == 3
add(1, 2.0);   // == 3.0
add(1.5, 1.5); // == 3.0

for loops over a range

std::array<int, 5> a {1, 2, 3, 4, 5};
for (int& x : a) x *= 2;
// a == { 2, 4, 6, 8, 10 }
std::array<int, 5> a {1, 2, 3, 4, 5};
for (int x : a) x *= 2;
// a == { 1, 2, 3, 4, 5 }

smart pointers

  • C++11 adds new smart pointers to the language: std::unique_ptr, std::shared_ptr, std::weak_ptr.

  • A std::auto_ptr deprecated and eventually removed in C++17.

std::unique_ptr<int> i_ptr1{new int{5}}; // Не рекомендуется 
auto i_ptr2 = std::make_unique<int>(5);  // Так лучше

template <typename T>
struct demo
{
    T m_var;

    demo(T var) : m_var(var){};
};

auto i_ptr3 = std::make_shared<demo<uint32_t>>(4);

nullptr

  • C++11 added a new null pointer type designed to replace the C NULL macro.

  • nullptr has type std::nullptr_t and can be implicitly converted to non-null pointer types, and unlike NULL, is not convertible to integral types, with the exception of bool.

void foo(int);
void foo(char*);
foo(NULL); // ошибка -- неоднозначность
foo(nullptr); // вызывает foo(char*)

Strongly Typed Enums

enum class STATUS_t : uint32_t
{
    PASS = 0,
    FAIL,
    HUNG
};

STATUS_t STATUS = STATUS_t::PASS;
STATUS - 1; // больше не валидно, начиная с C++11
  • Type-safe enums that solve many problems with C enums, including implicit conversions, arithmetic operations, inability to specify a base type, scope pollution, etc.

Cast

  • A C-style cast only changes the type, not the data itself. While the old C++ had a slight type safety bias, it provided the feature of specifying a type conversion operator/function. But it was an implicit type conversion. Starting with C++11, type conversion functions can now be made explicit with the specifier explicit in the following way:

struct demo
{
    explicit operator bool() const { return true; }
};

demo d;
if (d);                             // OK, вызывает demo::operator bool()
bool b_d = d;                       // ОШИБКА: не может преобразовать 'demo' в 'bool' во время инициализации
bool b_d = static_cast<bool>(d);    // OK, явное преобразование, вы знаете, что делаете
  • If the code above seems strange to you, then you can read my detailed discussion of this topic – “Type casting in C++“.

Move semantics

  • When the object is destroyed or no longer used after the expression is executed, it is more appropriate to move (move) resource rather than copying it.

  • Copying includes unnecessary overhead such as allocating memory, freeing and copying the contents of memory, and so on.

  • Consider the following function, which swaps two values:

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // теперь у нас есть две копии a
    a = b;      // теперь у нас есть две копии b (+ отброшена копия a)
    b = tmp;    // теперь у нас есть две копии tmp (+ отброшена копия b)
}
template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}
  • Now imagine what happens when Т it is, let’s say vector<int> size n. And n is big enough.

  • In the first version you are reading and writing 3*n elements, in the second version you are actually reading and writing only 3 pointers to vector buffers plus 3 buffer sizes.

  • Of course class Т must know how to move; your class should have move assignment operator and move constructor for class Тto make it work.

  • This feature will give you a significant performance boost, which is exactly why people use C++ (i.e. to squeeze the last 2-3 drops of speed).

Universal Links

  • Known in official terminology as forwarding references (transferred links). A universal reference is declared using the syntax Т&&Where Т is a template type parameter, or with auto&&. They, in turn, serve as the foundation for two other big features:

    • move-semantics

    • AND perfect forwardingthe ability to pass arguments that are either lvalueor rvalue.

Universal references allow you to refer to a binding either to lvalueor to rvalue depending on the type. Universal Links follow the rules link folding:

  1. T& & becomes T&

  2. T& && becomes T&

  3. T&& & becomes T&

  4. T&& && becomes T&&

Deriving a template type parameter with lvalue and rvalue:

// Начиная с C++14 и далее:
void f(auto&& t) {
  // ...
}

// Начиная с C++11 и далее:
template <typename T>
void f(T&& t) {
  // ...
}

int x = 0;
f(0); // выводится как f(int&&)
f(x); // выводится как f(int&)

int& y = x;
f(y); // выводится как f(int& &&) => f(int&)

int&& z = 0; // ПРИМЕЧАНИЕ: z — это lvalue типа int&amp;&amp;.
f(z); // выводится как f(int&& &) => f(int&)
f(std::move(z)); // выводится как f(int&& &&) => f(int&&)
  • If this seems complicated and strange to you, then for a start read itand then come back.

Variable Argument Templates

void print() {}

template <typename First, typename... Rest>
void print(const First &first, Rest &&... args)
{
    std::cout << first << std::endl;
    print(args...);
}

print(1, "lol", 1.1);
  • Syntax … creates parameter package or extend an existing one. Template parameter package is a template parameter that takes zero or more template arguments (untyped objects, types, or templates). C++ template with at least one parameter package is called variable template with a variable number of arguments (variadic template).

constexpr

constexpr uint32_t fibonacci(uint32_t i)
{
    return (i <= 1u) ? i : (fibonacci(i - 1) + fibonacci(i - 2));
}

constexpr auto fib_5th_term = fibonacci(6); // равноценно auto fib_5th_term = 8
  • Constant expressions are expressions that are evaluated by the compiler at compile time. In the example above, the function fibonacci executed/calculated by the compiler at compile time, and will be replaced by the result in the place call.

  • I wrote a detailed article covering this topic, “Using const and constexpr in C++“.

Removed and default functions

struct demo
{
    demo() = default;
};

demo d;

You can restrict a specific operation or method object instantiationby simply removing the corresponding method as shown below:

class demo
{
    int m_x;

public:
    demo(int x) : m_x(x){};
    demo(const demo &) = delete;
    demo &operator=(const demo &) = delete;
};

demo obj1{123};
demo obj2 = obj1; // ОШИБКА -- вызов удаленного конструктора копирования
obj2 = obj1;      // ОШИБКА -- оператор = удален

In old C++ you had to make it private. But now you have a compiler directive at your disposal delete.

Delegating Constructors

struct demo
{
    int m_var;
    demo(int var) : m_var(var) {}
    demo() : demo(0) {}
};

demo d;
  • In old C++, you need to create a member function for initialization and call it from all constructors to achieve universal initialization.

  • But starting with C++11, constructors can now call other constructors from the same class using an initializer list.

Lambda Expressions

auto generator = [i = 0]() mutable { return ++i; };
cout << generator() << endl; // 1
cout << generator() << endl; // 2
cout << generator() << endl; // 3
  • I think this feature needs no introduction and is a favorite among other features.

  • Now you can declare functions anywhere. And it won’t cost you any additional overhead.

  • I wrote a separate article on this topic – “Understanding lambda expressions in C++ with examples“.

Branch statements with an initializer

  • In earlier versions of C++, the initializer was either declared before the statement and leaked into the outer scope, or an explicit scope was used.

  • C++17 has a new form if/switchwhich can be written more compactly, and better scoping makes some previously error-prone constructs a bit more robust:

switch (auto STATUS = window.status()) // Объявляем объект прямо в операторе ветвления
{
case PASS:// делаем что-то
    break;
case FAIL:// делаем что-то
    break;
}
{
    auto STATUS = window.status();
    switch (STATUS)
    {
    case PASS: // делаем что-то
        break;
    case FAIL: // делаем что-то
        break;
    }
}

std::tuple

auto employee = std::make_tuple(32, " Vishal Chovatiya", "Bangalore");
cout << std::get<0>(employee) << endl; // 32
cout << std::get<1>(employee) << endl; // "Vishal Chovatiya"
cout << std::get<2>(employee) << endl; // "Bangalore"
  • Tuples are a set of heterogeneous values ​​of a fixed size. Access to elements std::tuple produced using std::tie or std::get.

  • You can also snag arbitrary and heterogeneous return values ​​like this:

auto get_employee_detail()
{
    // делаем что-нибудь . . . 
    return std::make_tuple(32, " Vishal Chovatiya", "Bangalore");
}

string name;
std::tie(std::ignore, name, std::ignore) = get_employee_detail();
  • Use std::ignore as a placeholder for ignored values. In C++17, one should use instead structured bindings.

Deriving a class template argument

std::pair<std::string, int> user = {"M", 25}; // раньше
std::pair user = {"M", 25};                   // C++17

std::tuple<std::string, std::string, int> user("M", "Chy", 25); // раньше
std::tuple user2("M", "Chy", 25);                               // выведение в действии!
  • Template argument auto-inference is very similar to how it’s done for functions, but now also includes class constructors.

A couple of words in conclusion

Here we have only scratched the surface of the vast set new features and the possibility of their application. There’s a lot more to be found in modern C++, but you can still consider this collection a good starting point. Modern C++ is expanding not only in terms of syntax, but also adding many more other features such as unordered containers, streams, regular expression, Chrono, random number generator/distributor, Exception Handling and many new STL algorithms (for example, all_of(), any_of(), none_of()etc).

May C++ be with you!


Tomorrow evening there will be an open session dedicated to Boost. In the lesson, you will learn how to include Boost in a project using cmake; learn more about the Boost libraries and learn how to use them. You can sign up for a lesson on the course page “C++ Developer. Professional”.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *