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
typedef
however 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 withdecltype
For 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 sayvector<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 withauto&&
. They, in turn, serve as the foundation for two other big features:move-semantics
AND perfect forwardingthe ability to pass arguments that are either
lvalue
orrvalue
.
Universal references allow you to refer to a binding either to lvalue
or to rvalue
depending on the type. Universal Links follow the rules link folding:
T& &
becomesT&
T& &&
becomesT&
T&& &
becomesT&
T&& &&
becomesT&&
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&&.
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 usingstd::tie
orstd::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”.