21 новая фича C++, которые вам обязательно пригодятся

от автора

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

Сообщество C++ дополняет стандарт чаще, чем Apple выпускает новые iPhone. Благодаря этому C++ теперь больше похож на большого слона, а съесть целого слона за один присест невозможно. Вот почему я решил написать эту статью, чтобы дать вашему путешествию по современному C++ своего рода отправную точку. Моя целевая аудитория здесь — люди, которые переходят со старого (т.е. 98/03) С++ на современный (т.е. 2011 и далее) С++.

Я отобрал ряд фич современного C++ и постарался объяснить их на лаконичных примерах, чтобы вы научились определять места, где их можно использовать.

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

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

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

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

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

template <typename T> using dyn_arr = std::vector<T>; dyn_arr<int> nums; // эквивалентно std::vector<int>  using func_ptr = int (*)(int);
  • Семантически похоже на использование typedef, однако псевдонимы типов легче читаются и совместимы с шаблонами С++. Поблагодарите С++11.

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

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 в С++”.

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

Раньше вам нужно было инициализировать поля их значениями по умолчанию в конструкторе или в списке инициализации. Но начиная с 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 члены класса, как показано выше.

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.

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.

Циклы 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 }

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

  • C++11 добавляет в язык новые умные указатели: std::unique_ptr, std::shared_ptr, std::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);

nullptr

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

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

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

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

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

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

  • Приведение в стиле 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, явное преобразование, вы знаете, что делаете
  • Если приведенный выше код кажется вам странным, то можете прочитать мой подробный разбор этой темы — “Приведение типов в С++”.

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 капли скорости).

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

  • В официальной терминологии известные как 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&&)
  • Если вам это кажется сложным и странным, тогда для начала прочитайте это, а затем возвращайся обратно.

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

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

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 выполняется/вычисляется компилятором во время компиляции, и будет заменена на результат в вызове места.

  • Я написал подробную статью, раскрывающую эту тему, “Использование const и constexpr в С++”.

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

struct demo {     demo() = default; };  demo d;

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

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.

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

struct demo {     int m_var;     demo(int var) : m_var(var) {}     demo() : demo(0) {} };  demo d;
  • В старом C++ вам нужно создавать функцию-член для  инициализации и вызывать ее из всех конструкторов для достижения универсально инициализации.

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

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

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

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

  • Я написал отдельную статью на эту тему — “Разбираемся с лямбда-выражениями в C++ на примерах”.

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

  • В более ранних версиях 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;     } }

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, вместо этого следует использовать структурированные привязки.

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

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++!


Завтра вечером пройдет открытое занятие, посвященное Boost. На уроке вы узнаете, как подключать Boost в проект с помощью cmake; познакомитесь подробнее с библиотеками Boost и научитесь их использовать. Записаться на урок можно на странице курса «C++ Developer. Professional».


ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/741428/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *