21 фича современного C++


В этой статье вашему вниманию будут представлены 21 новая фича современного C++, которые помогут сделать ваш проект лучше, а работу над ним легче.

1. Разделители разрядов чисел

 

int no = 1'000'000;                      // визуальное разделение единиц, тысяч, миллионов и т.д.
long addr = 0xA000'EFFF;                 // визуальное разделение 32-битного адреса на
uint32_t binary = 0b0001'0010'0111'1111; // удобочитаемые сегменты
  • Раньше вам нужно было считать цифры или нули, но, начиная с C++14, вы можете сделать большие числа намного нагляднее.

  • Эта фича помогает облегчить навигацию по словам и цифрам. Или, допустим, вы можете повысить читаемость номера кредитной карты или социального страхования.

  • Благодаря сгруппированным разрядам, ваш код станет немного выразительнее.

 

2. Псевдонимы типов

 

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

using func_ptr = int (*)(int);
  • Семантически похоже на использование typedef, однако псевдонимы типов легче читаются и совместимы с шаблонами С++. Поблагодарите С++11.

 

3. Пользовательские литералы

 

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;
  • По большей части это будут какие-нибудь реальные единицы, такие как kb, mb, км, см, рубли, доллары, евро и т.д. Пользовательские литералы позволяют вам не определять функции, для выполнения преобразования единиц измерения во время выполнения, а работать с ним как с другими примитивными типами.

  • Очень удобно для единиц и измерения.

  • Благодаря добавлению constexpr вы можете добиться нулевого влияния на производительность во время выполнения, что мы увидим позже в этой статье, и более подробно вы можете почитать об этом в другой статье, которую я написал, — “Использование const и constexpr в С++”.

 

4. Унифицированная инициализация и инициализация нестатических членов

 

Раньше вам нужно было инициализировать поля их значениями по умолчанию в конструкторе или в списке инициализации. Но начиная с C++11 можно задавать обычным переменным-членам класса (тем, которые не объявлены с ключевым словом static) инициализирующее значение по умолчанию, как показано ниже:

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};
  • Это особенно полезно, когда в качестве полей выступают сразу несколько вложенных объектов, определенных, как показано ниже:

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; // не требуется для статических константных полей
  • Вы также можете инициализировать во время объявления const static члены класса, как показано выше.

 

5. 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};
  • Присваивайте значения контейнерам непосредственно с помощью списка инициализаторов, как это можно делать с C-массивами.

  • Это справедливо и для вложенных контейнеров. Скажите спасибо С++11.

 

6. 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-типизированные переменные выводятся компилятором на основе типа их инициализатора.

  • Чрезвычайно полезно с точки зрения удобочитаемости, особенно для сложных типов:

// 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); // альтернатива
  • Функции также могут выводить тип возвращаемого значения с помощью auto. В C++11 тип возвращаемого значения должен быть указан либо явно, либо с помощью decltype, например:

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
  • Приведенная выше форма определения возвращаемого типа называется trailing return type, т.е. -> return-type.

 

7. Циклы for по диапазону

 

  • Синтаксический сахар для перебора элементов контейнера.

std::array<int, 5> a {1, 2, 3, 4, 5};
for (int& x : a) x *= 2;
// a == { 2, 4, 6, 8, 10 }
  • Обратите внимание на разницу при использовании int в противовес int&:

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

 

8. Умные указатели

 

  • C++11 добавляет в язык новые умные указатели: std::unique_ptrstd::shared_ptrstd::weak_ptr.

  • А std::auto_ptr устарел, и в конечном итоге удален в 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);
  • Гайдлайны ISO CPP рекомендуют избегать явных вызовов new и delete, выразив это в правиле “никаких голых new”.

 

9. nullptr

 

  • C++11 добавил новый тип пустого указателя, предназначенный для замены макроса C NULL.

  • nullptr имеет тип std::nullptr_t и может быть неявно преобразован в типы непустых указателей, и в отличие от NULL, не конвертируем в целочисленные типы, за исключением bool.

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

 

10. Строго типизированные перечисления

 

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

STATUS_t STATUS = STATUS_t::PASS;
STATUS - 1; // больше не валидно, начиная с C++11
  • Типобезопасные перечисления, которые решают множество проблем с C-перечислениями, включая неявные преобразования, арифметические операции, невозможность указать базовый тип, загрязнение области видимости и т.д.

 

11. Приведение типов

 

  • Приведение в стиле C изменяет только тип, не затрагивая сами данные. В то время как старый C++ имел небольшой уклон в типобезопасность, он предоставлял фичу указания оператора/функции преобразования типа. Но это было неявное преобразование типов. Начиная с C++11, функции преобразования типов теперь можно сделать явными с помощью спецификатора explicit следующим образом:

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, явное преобразование, вы знаете, что делаете

 

 

12. Move-семантика

 

  • Когда объект будет уничтожен или не будет более использоваться после выполнения выражения, целесообразнее переместить (move) ресурс, а не копировать его.

  • Копирование включает в себя ненужные накладные расходы, такие как выделение памяти, высвобождение и копирование содержимого памяти и т.д.

  • Рассмотрим следующую функцию, меняющую местами два значения:

template <class T>
swap(T& a, T& b) {
T tmp(a); // теперь у нас есть две копии a
a = b; // теперь у нас есть две копии b (+ отброшена копия a)
b = tmp; // теперь у нас есть две копии tmp (+ отброшена копия b)
}
  • Использование move позволяет вам напрямую обменивать ресурсы вместо их копирования:

template <class T>
swap(T& a, T& b) {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
  • А теперь представьте, что происходит, когда Т это, скажем, vector<int> размера n. И n достаточно велико.

  • В первой версии вы читаете и записываете 3*n элементов, во второй версии вы в по сути читаете и записываете только 3 указателя на буферы векторов плюс 3 размера буферов.

  • Конечно, класс Т должен знать, как ему перемещаться; ваш класс должен иметь оператор присваивания перемещением и конструктор перемещения для класса Т, чтобы это работало.

  • Эта фича даст вам значительный прирост в производительности — именно то, поэтому люди используют C++ (т.е., чтобы выжать последние 2-3 капли скорости).

 

13. Универсальные ссылки

 

  • В официальной терминологии известные как forwarding references (передаваемые ссылки). Универсальная ссылка объявляется с помощью синтаксиса Т&&, где Т является шаблонным параметром типа, или с помощью auto&&. Они в свою очередь служат фундаментом для двух других крупных фич:

    • move-семантика

    • И perfect forwarding, возможность передавать аргументы, которые являются либо lvalue, либо rvalue.

Универсальные ссылки позволяют ссылаться на привязку либо к lvalue, либо к rvalue в зависимости от типа. Универсальные ссылки следуют правилам свертывания ссылок:

  1. T& & становится  T&  

  2. T& && становится T&

  3. T&& & становится T&

  4. T&& && становится T&&

Вывод шаблонного параметра типа с lvalue ​​и 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&&)

 

 

14. Шаблоны с переменным количеством аргументов

 

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);
  • Синтаксис ... создает пакет параметров или расширяет уже существующий. Шаблонный пакет параметров — это шаблонный параметр, который принимает ноль или более аргументов-шаблонов (нетипизированных объектов, типов или шаблонов). Шаблон С++ с хотя бы одним пакетом параметров называется вариативный шаблоном с переменным количеством аргументов (variadic template).

 

15. 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
  • Константные выражения — это выражения, вычисляемые компилятором во время компиляции. В приведенном выше примере функция fibonacci выполняется/вычисляется компилятором во время компиляции, и будет заменена на результат в вызове места.

 

16. Удаленные и дефолтные функции

 

struct demo
{
demo() = default;
};

demo d;
  • У вас вполне закономерно может возникнуть вопрос, зачем вам писать 8+ букв (т.е. = default;), когда можно просто использовать {}, т.е. пустой конструктор? Никто вас не останавливает. Но подумай о конструкторе копирования, операторе копирования присваиванием, и т.д.

  • Пустой конструктор копирования, например, не то же самое, что конструктор копирования по умолчанию (который будет выполнять почленную копию всех членов).

Вы можете ограничить определенную операцию или способ инстанцирования объекта, просто удалив соответствующий метод, как показано ниже:

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; // ОШИБКА -- оператор = удален

В старом С++ вы должны были сделать его приватным. Но теперь в вашем распоряжении есть директива компилятора delete.

 

 

17. Делегирование конструкторов

 

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

demo d;
  • В старом C++ вам нужно создавать функцию-член для  инициализации и вызывать ее из всех конструкторов для достижения универсально инициализации.

  • Но начиная с C++11 конструкторы теперь могут вызывать другие конструкторы из того же класса с помощью списка инициализаторов.

 

18. Лямбда-выражения

 

auto generator = [i = 0]() mutable { return ++i; };
cout << generator() << endl; // 1
cout << generator() << endl; // 2
cout << generator() << endl; // 3
  • Я думаю, что эта фича не нуждается в представлении и является фаворитом среди других фич.

  • Теперь вы можете объявлять функции где угодно. И это не будет стоить вам никаких дополнительных накладных расходов. 

 

19. Операторы ветвления с инициализатором

 

  • В более ранних версиях C++ инициализатор либо объявлялся перед оператором и просачивался во внешнюю область видимости, либо использовалась явная область видимости.

  • В C++17 появилась новая форма if/switch, которую можно записать более компактно, а улучшенный контроль области видимости делает некоторые ранее подверженные ошибкам конструкции немного более надежными:

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

{
auto STATUS = window.status();
switch (STATUS)
{
case PASS: // делаем что-то
break;
case FAIL: // делаем что-то
break;
}
}

 

20. 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"
  • Кортежи представляют собой набор разнородных значений фиксированного размера. Доступ к элементам std::tuple производится с помощью std::tie или std::get.

  • Вы также можете выхватывать произвольные и разнородные возвращаемые значения следующим образом:

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

string name;
std::tie(std::ignore, name, std::ignore) = get_employee_detail();
  • Используйте std::ignore в качестве плейсхолдера для игнорируемых значений. В С++ 17, вместо этого следует использовать структурированные привязки.

 

21. Выведение аргумента шаблона класса

 

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); // выведение в действии!
  • Автоматическое выведение аргументов шаблона очень похоже на то, как это делается для функций, но теперь также включает и конструкторы классов.

 

Пара слов в заключение 

 

Здесь мы только слегка коснулись огромного набора новых фич и возможности их применения. В современном C++ можно найти еще очень много чего, но тем не менее вы можете считать этот набор хорошей отправной точкой. Современный C++ расширяется не только с точки зрения синтаксиса, но также добавляется гораздо больше других функций, таких как неупорядоченные контейнеры, потоки, регулярное выражение, Chrono, генератор/распределитель случайных чисел, обработка исключений и множество новых алгоритмов STL (например, all_of()any_of()none_of(), и т.д).

Да пребудет с вами C++!



Вернуться назад