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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

  • Я уже писал об этом в статье “Разбираемся с unique_ptr в С++ на примерах”.

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;
  • У вас вполне закономерно может возникнуть вопрос, зачем вам писать 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.

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

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/


Интересные статьи

Интересные статьи

Вы видели отчёты крупных книжных онлайн-магазинов и маркетплейсов о самых читаемых книгах 2022 года? Скажу откровенно: любопытно, но мне бы не хотелось получить такой список литературы на лето. Причём...
«В жизни, в отличие от шахмат, игра продолжается после мата» — Айзек АзимовПочему ученые в области информатики помешаны на создании искусственного интеллекта, способного играть в человеческие игры (на...
Всемирная сеть построена на технологиях полувековой давности. Некоторые из них оказались не готовы к работе с миллиардами вычислительных устройств, другие [такие, как IPv4] вообще задумывали как време...
Когда возникает проблема, требующая анализа, компании необходимо использовать инструменты анализа основной причины, чтобы рассмотреть нечто большее, чем «некоторые образо...
Клиенту и разработчику довольно трудно найти общий язык: они мыслят разными категориями и оперируют разными терминами. Мы составили небольшой словарь наиболее распространённых понятий в м...