Как стать автором
Обновить
219.09
Яндекс Практикум
Помогаем людям расти

Стандарт C++20: обзор новых возможностей C++. Часть 6 «Другие фичи ядра и стандартной библиотеки. Заключение»

Время на прочтение 13 мин
Количество просмотров 19K


25 февраля автор курса «Разработчик C++» в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ — Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся на почти 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

  1. Модули и краткая история C++.
  2. Операция «космический корабль».
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

Это шестая, заключительная часть. Она рассказывает о других нововведениях ядра и стандартной библиотеки, добавленных Стандартом C++20.

Другие фичи ядра


Я рассказал о самых значительных нововведениях Стандарта, но это только капля в море C++20. Перейдём к менее глобальным, но не менее интересным вещам. Подробно останавливаться на каждой не буду, потому что цель обзора — рассказать пусть не всё, но обо всём.

Шаблоны


Многое из нововведений Стандарта касается шаблонов. В C++ шаблонные аргументы делятся на два вида: типовые и нетиповые. Типовые названы так неспроста: их значение — тип данных, например int, контейнер, ваш класс, указатель. Нетиповые шаблонные аргументы — это обычные значения, вычисляемые на этапе компиляции, например число 18 или true. В C++17 возможности нетиповых шаблонных параметров были сильно ограничены. Это могло быть числовое значение, bool, enum-тип или указатель. C++20 расширил список и позволил передавать в качестве шаблонного аргумента объект пользовательского класса или структуры. Правда, объект должен удовлетворять ряду ограничений.

Пример:

struct T {
    int x, y;
};

template<T t>
int f() {
    return t.x + t.y;
}

int main() {
    return f<{1,2}>();
}

GCC 😊, CLANG 😊, VS 😊

Ещё из нового — крутые возможности вывода шаблонного типа класса. Не буду комментировать, просто оставлю пример. Кому интересно — разберитесь:

template<class T> struct B {
    template<class U> using TA = T;
    template<class U> B(U, TA<U>);  //#1
};
 
B b{(int*)0, (char*)0}; // OK, выведен B<char*>

GCC 😊, CLANG 😊, VS 😊

Лямбды


Раньше в лямбда-функциях было три вида скобок. Теперь их может быть четыре — стало возможным писать явные параметры для лямбда-функций:

  1. квадратные скобки для переменных связывания,
  2. угловые скобки для шаблонных параметров,
  3. круглые скобки для списка аргументов,
  4. фигурные скобки для тела функции.

Порядок важен: если его перепутать, будет ошибка. Если шаблонных параметров нет, то угловые скобки не пишите, потому что пустыми они быть не могут. Кстати, эта фича уже нормально поддерживается во всех компиляторах.

int main() {
    auto lambda = []<class T>(T x, T y){ 
                               return x * y - x - y; };
    std::cout << lambda(10, 12);
}

GCC 😊, CLANG 😊, VS 😊

А ещё лямбда-функция, у которой нет состояния, теперь допускает копирование и конструирование, то есть можно вообще вывести тип функции и создать значение этого типа. Круто, потому что можно указать тип лямбда-функции как тип компаратора в контейнере.

int main() {
    using LambdaPlus3 = decltype([](int x) {
    	return x + 3;
    });

    LambdaPlus3 l1;
    auto l2 = l1;

    std::cout << l2(10) << std::endl; // 13
}

GCC 😊, CLANG 😊, VS 😊

Также в C++20 можно использовать лямбды в невычислимом контексте, например внутри sizeof.

Compile-time


Появился новый вид переменных — constinit. Это некоторая замена static-переменным. Локальные static-переменные могут быть проинициализированы во время первого обращения, что иногда нежелательно. А вот constinit инициализируются по-настоящему статически. Если компилятор не смог проинициализировать такую переменную во время компиляции, код не соберётся. По Стандарту constinit-переменная может быть также thread_local.

const char* g() { return "dynamic initialization"; }
constexpr const char* f(bool p) { return p ? "constant "
      "initializer" : g(); }
 
constinit const char* c = f(true); // OK

int main() {
    static constinit int x = 3;
    x = 5; // OK
}

Удивительно, но constinit-переменная не обязана быть константной. Константным и constexpr обязан быть её инициализатор.

GCC 😊, CLANG 😊, VS 😊

И у функций тоже есть новый вид — consteval. Это функции, которые вычисляется исключительно во время компиляции. В отличие от constexpr, которые могут вызываться как в run-time, так и в compile-time, эти функции не получится даже вызвать во время выполнения, будет ошибка.

consteval int sqr(int n) {
    return n * n;
}
constexpr int r = sqr(100);  // OK
 
int x = 100;
int r2 = sqr(x); // <-- ошибка: результат не константа

consteval int sqrsqr(int n) {
    return sqr(sqr(n));
}
constexpr int dblsqr(int n) { return 2*sqr(n); } // <-- ошибка, constexpr может 
                                                 // вычисляться в run-time

Многие слушатели вебинара не поняли смысла этой фичи. Зачем нужен ещё один constexpr. Но представьте действие, которое имеет смысл только во время компиляции. Например, вы положили в проект файлы ресурсов и во время компиляции их читаете. Пока что так делать нельзя, поэтому просто представьте. В run-time этих ресурсов уже не будет.

GCC 😊, CLANG 😐, VS 😊

В C++20 очень существенно расширили сам constexpr, внутри него теперь можно вызывать даже виртуальные функции! А ещё, что особенно замечательно, многие контейнеры стали поддерживать constexpr.

Новые синтаксические конструкции


Мы ждали их ещё с 1999 года. Всё это время программисты на чистом C дразнили нас, демонстрируя, как они могут ими пользоваться, а мы — нет. Вы уже поняли, о чём речь? Конечно, о designated initializers, или обозначенных инициализаторах!

Теперь, конструируя объект структуры, можно явно написать, какому полю присваивается какое значение. Это очень круто. Ведь для структур с 10–15 полями понять, что чему соответствует, практически невозможно. А такие структуры я видел. Кроме того, поля можно пропускать. Но вот порядок менять нельзя. И нельзя ещё несколько вещей, которые можно в C. Они приведены в примере.

struct A { int x; int y; int z; }; 
A b{ .x = 1, .z = 2 };
A a{ .y = 2, .x = 1 }; // <-- ошибка – нарушен порядок

struct A { int x, y; };
struct B { struct A a; };
int arr[3] = {[1] = 5};     // <-- ошибка – массив   
struct B b = {.a.x = 0};    // <-- ошибка – вложенный
struct A a = {.x = 1, 2};   // <-- ошибка – два вида инициализаторов

GCC 😊, CLANG 😊, VS 😊

Ещё крутое нововведение, которое забыли добавить девять лет назад — инициализатор в for по диапазону. Ничего хитрого тут нет, просто дополнительная возможность для for:

#include <vector>
#include <string>
#include <iostream>

int main() {
    using namespace std::literals;
    std::vector v{"a"s, "b"s, "c"s};

    for(int i=0; const auto& s: v) {
        std::cout << (++i) << " " << s << std::endl;
    }
}


GCC 😊, CLANG 😊, VS 😊

Новая конструкция using enum. Она делает видимыми без квалификации все константы из enum-а:

enum class fruit { orange, apple };
enum class color { red, orange };
void f() {
    using enum fruit; // OK
    using enum color; // <-- ошибка – конфликт
}


GCC 😊, CLANG 😔, VS 😊

Другое


  • Операция запятая внутри [] объявлена как deprecated. Намёк на что-то интересное в будущем.

GCC 😊, CLANG 😊, VS 😊
  • Запрет некоторых действий с volatile-переменными. Эти операции тоже объявлены как deprecated. Переменные volatile сейчас часто используются не по назначению. Видимо, комитет решил с этим бороться.

GCC 😊, CLANG 😊, VS 😊
  • Агрегатные типы можно инициализировать простыми круглыми скобками — ранее были допустимы только фигурные.

GCC 😊, CLANG 😔, VS 😊
  • Появился уничтожающий оператор delete, который не вызывает деструктор, а просто освобождает память.

GCC 😊, CLANG 😊, VS 😊
  • Слово typename в некоторых случаях можно опускать. Лично меня нервировала необходимость писать его там, где оно однозначно нужно.

GCC 😊, CLANG 😔, VS 😊
  • Теперь типы char16_t и char32_t явно обозначают символы в кодировках, соответственно UTF-16 и UTF-32. Ещё добавили новый тип char8_t для UTF-8.

GCC 😊, CLANG 😊, VS 😊
  • Различные технические нововведения. Если я что-то упустил — пишите в комменты.

Другие фичи стандартной библиотеки


Это были нововведения ядра, то есть то, что меняет синтаксис самого C++. Но ядро — даже не половина Стандарта. Мощь C++ — также в его стандартной библиотеке. И в ней тоже огромное число нововведений.

  • В <chrono> наконец-то добавлены функции работы с календарём и часовыми поясами. Появились типы для месяца, дня, года, новые константы, операции, функции для форматирования, конвертации часовых поясов и много магии. Этот пример из cppreference оставлю без комментариев:

#include <iostream>
#include <chrono>
using namespace std::chrono;
 
int main() {
    std::cout << std::boolalpha;
 
    // standard provides 2021y as option for std::chrono::year(2021)
    // standard provides 15d as option for std::chrono::day(15)
 
    constexpr auto ym {year(2021)/8};
    std::cout << (ym == year_month(year(2021), August)) << ' ';
 
    constexpr auto md {9/day(15)};
    std::cout << (md == month_day(September, day(15))) << ' ';
 
    constexpr auto mdl {October/last};
    std::cout << (mdl == month_day_last(month(10))) << ' ';
 
    constexpr auto mw {11/Monday[3]};
    std::cout << (mw == month_weekday(November, Monday[3])) << ' ';
 
    constexpr auto mwdl {December/Sunday[last]};
    std::cout << (mwdl == month_weekday_last(month(12), weekday_last(Sunday))) << ' ';
 
    constexpr auto ymd {year(2021)/January/day(23)};
    std::cout << (ymd == year_month_day(2021y, month(January), 23d)) << '\\n';
}

// вывод: true true true true true true

GCC 😐, CLANG 😐, VS 😊

  • Целая новая библиотека format. Про неё также можно послушать в докладе у Антона Полухина. Теперь в C++ есть современная функция для форматирования строк с плейсхолдерами. Библиотека позволит делать подобную магию:

auto s1 = format("The answer is {}.", 42); // s1 == "The answer is 42."

auto s2 = format("{1} from {0}", "Russia", "Hello"); // s2 == "Hello from Russia"

int width = 10;
int precision = 3;
auto s3 = format("{0:{1}.{2}f}", 12.345678, width, precision);
// s3 == "    12.346"

Она обещает быть куда более производительной, чем вывод в поток stringstream, но тем не менее не лишена проблем. Первая проблема: format не проверяет все ошибки, и вообще, на данном этапе не разбирает формат в compile-time. Это очень огорчает.
Сейчас в комитете хотят это поправить с бэкпортированием функционала в C++20.
Антон Полухин
Вторая проблема: пока не реализован ни в одной стандартной библиотеке, проверить в деле нельзя.

За время, прошедшее с вебинара, format успели реализовать в Visual Studio.

GCC 😔, CLANG 😔, VS 😊

  • Потрясающие новости: теперь в Стандарте есть π. Помимо него добавлено обратное π, число Эйлера, логарифмы некоторых чисел, корни и обратные корни, корень из π вместе с обратным, постоянная Эйлера — Маскерони и золотое сечение. Все они доступны при подключении <numbers> в пространстве имён std::numbers.

GCC 😊, CLANG 😊, VS 😊

  • Новые алгоритмы: shift_left и shift_right. Они сдвигают элементы диапазона на заданное число позиций. При этом закручивания не происходит: элементы, уходящие на край, не телепортируются в другой конец, а уничтожаются. С другого края возникают пустые элементы — из них был сделан move.

GCC 😊, CLANG 😊, VS 😊

  • Новые функции midpoint и lerp для вычисления среднего и средневзвешенного. Вообще, это не так сложно было писать самим, но мы писали каждый раз, а теперь такая функция доступна из коробки.

GCC 😊, CLANG 😔, VS 😊

  • Ещё одна несложная функция — in_range. Она позволяет проверить, представимо ли целое число значением другого типа:

#include <utility>
#include <iostream>
 
int main() {
    std::cout << std::boolalpha;
 
    std::cout << std::in_range<std::size_t>(-1) 
              << '\\n'; // false, так как отрицательные числа не представимы в size_t

    std::cout << std::in_range<std::size_t>(42) 
              << '\\n'; // true
}

GCC 😊, CLANG 😊, VS 😊

  • make_shared стал поддерживать создание массивов.

GCC 😔, CLANG 😔, VS 😊

  • Добавились операции сравнения для неупорядоченных контейнеров unordered_map и unordered_set.

GCC 😔, CLANG 😊, VS 😊

  • Новая функция std::to_array делает array из C-массива или строкового литерала.

GCC 😊, CLANG 😊, VS 😊

  • В заголовочный файл version добавили макросы для проверки наличия фич стандартной библиотеки. Уже были макросы для фич ядра, но теперь это распространяется и на стандартную библиотеку.

GCC 😊, CLANG 😊, VS 😊

  • Отличное нововведение: многие функции и методы стали constexpr. Теперь его поддерживают контейнеры, такие как string, vector.

Все алгоритмы из <algorithm>, не выделяющие память, стали constexpr. Можно сортировать массивы и делать бинарный поиск на этапе компиляции :)
Антон Полухин

GCC 😊, CLANG 😊, VS 😊

  • Появился новый тип span. Он задаёт указатель и число — количество элементов, на которые указывает указатель.

istream& read(istream& input, span<char> buffer) {
    input.read(buffer.data(), buffer.size());
    return input;
}

ostream& write(ostream& out, std::span<const char> buffer) {
    out.write(buffer.data(), buffer.size());
    return out;
}

std::vector<char> buffer(100);
read(std::cin, buffer);

span позволяет заменить два параметра функции одним. Он чем-то похож на string_view — это тоже лёгкая оболочка, которая может представлять элементы контейнеров. Но набор допустимых контейнеров больше — это может быть любой линейный контейнер: вектор, std::array, string или C-массив. Ещё одно отличие от string_view — он позволяет модификацию элементов, если они не константного типа. Важно, что модификацию только самих элементов, но не контейнера.

GCC 😊, CLANG 😊, VS 😊

  • Ещё одно замечательное нововведение — файл <bit>. Он добавляет большое количество возможностей для манипуляций с беззнаковыми числами на битовом уровне. Теперь функции вида «определить количество единиц в бинарном числе» или двоичный логарифм доступны из коробки. Эти функции перечислены на слайде.




Также файл определяет новый тип std::endian. Он, например, позволяет определить, какая система записи чисел используется при компиляции: Little endian или Big endian. А вот функций для их конвертации я, к сожалению, не нашёл. Но в целом считаю, что <bit> — очень крутое нововведение.

GCC 😊, CLANG 😐, VS 😊

  • Дождётся тот, кто сильно ждёт! Этот цитатой можно описать многое из Стандарта C++20. Поздравляю всех, мы дождались: у string теперь есть методы starts_with и ends_with для проверки суффиксов и постфиксов. А также другие методы контейнеров и функции, с ними связанные:
    • метод contains для ассоциативных контейнеров. Теперь можно писать my_map.contains(x) вместо my_map.count(x) > 0 и всем сразу понятно, что вам нужно проверить наличие ключа;
    • версии функции std::erase и std::erase_if для разных контейнеров;
    • функция std::ssize для получения знакового размера контейнера.

GCC 😊, CLANG 😊, VS 😊

  • Добавлена функция assume_aligned — она возвращает указатель, про который компилятор будет считать, что он выровнен: его значение кратно числу, которое мы указали в качестве шаблона у аргумента assume_aligned.

void f(int* p) {
   int* p1 = std::assume_aligned<256>(p);
}

Если указатель окажется в реальности невыровненным — добро пожаловать, неопределённое поведение. Выровненность указателя позволит компилятору сгенерировать более эффективный код векторизации.

GCC 😊, CLANG 😔, VS 😊

  • Добавился в Стандарт и новый вид потоков — osyncstream в файле syncstream. В отличие от всех остальных потоков, osyncstream — одиночка: у него нет пары на букву i. И это не случайно. Дело в том, что osyncstream — всего лишь обёртка. Посмотрите на код:

#include <thread>
#include <string_view>
#include <iostream>
using namespace std::literals;

void thread1_proc() {
    for (int i = 0; i < 100; ++i) {
        std::cout << "John has "sv << i 
                  << " apples"sv << std::endl;
    }
}

void thread2_proc() {
    for (int i = 0; i < 100; ++i) {
        std::cout << "Marry has "sv << i * 100
                  << " puncakes"sv << std::endl;
    }
}

int main() {
    std::thread t1(thread1_proc);
    std::thread t2(thread2_proc);
    t1.join(); t2.join();
}

В его выводе наверняка будет подобная абракадабра:

Marry has John has 24002 apples
John has 3 apples
John has 4 apples
John has 5 apples
John has 6 apples
John has 7 apples
 puncakesJohn has 8 apples

Вывод каждого отдельного элемента производится атомарно, но разные элементы всё равно перемешиваются между собой. osyncstream исправляет ситуацию:

...
#include <syncstream>
...
void thread1_proc() {
    for (int i = 0; i < 100; ++i) {
        std::osyncstream(std::cout) << "John has "sv << i 
                                    << " apples"sv << std::endl;
    }
}

void thread2_proc() {
    for (int i = 0; i < 100; ++i) {
        std::osyncstream(std::cout) << "Marry has "sv << i * 100 
                                    << " puncakes"sv << std::endl;
    }
}
...

Теперь все строки выведутся корректно. Этот поток не будет выводить ничего, пока не вызовется деструктор объекта: все данные он кеширует, а затем выполняет одну атомарную операцию вывода.

GCC 😊, CLANG 😔, VS 😊

  • В Стандарт добавился набор из шести новых функций для сравнения целых чисел:
    • cmp_equal,
    • cmp_not_equal,
    • cmp_less,
    • cmp_greater,
    • cmp_less_equal,
    • cmp_greater_equal.

Пользователь Хабра в комментариях к предыдущей части назвал их чуть ли не главной фичей нового Стандарта. Их особенность — в корректной работе с любыми типами аргументов, если это целые числа. Обычные операции сравнения могут давать неадекватный результат, особенно если вы сравниваете знаковое с беззнаковым:

-1 > 0u; // true

По правилам в подобном случае знаковый операнд преобразуется к беззнаковому значению: 0xFFFFFFFFu для 32-битного int. Функция cmp_greater позволит обойти эту особенность и выполнить настоящее математическое сравнение:

std::cmp_greater(-1, 0u); // false
GCC 😊, CLANG 😊, VS 😊

  • Ещё одно нововведение — source_location. Это класс, который позволит заменить макросы __LINE__, __FILE__, используемые при логировании. Статический метод current этого класса вернёт объект source_location, который содержит строку и название файла, в котором этот метод был вызван. Тут я задал вопрос. Какое число выведет функция со слайда?



Есть два варианта:

  • число 2, что соответствует строке, где source_location написан;
  • число 7, что соответствует строке, где функция log вызвана.

Правильный ответ — 7, где вызвана функция, хоть это и кажется неочевидным. Но именно благодаря этому обстоятельству source_location можно использовать как замену макросам __LINE__, __FILE__ для логирования. Потому что, когда мы логируем, нас интересует не где написана функция log, а откуда она вызвана.

GCC 😊, CLANG 😔, VS 😊

  • Закончим обзор на радостной ноте: в C++ существенно упростили многопоточное программирование.
    • Новый класс counting_semaphore — ждём, пока определённое количество раз разблокируют семафор.
    • Классы latch и barrier блокируют, пока определённое количество потоков не дойдёт до определённого места.
    • Новый вид потоков: jthread. Он делает join в деструкторе, не роняя вашу программу. Также jthread поддерживает флаг отмены, через который удобно прерывать выполнение треда — stop_token. С этим флагом связаны сразу несколько новых классов.
    • Ещё один новый класс atomic_ref — специальная ссылка, блокирующая операции других потоков с объектом.
    • Возможности atomic значительно расширены. Он теперь поддерживает числа с плавающей точкой и умные указатели, а также новые методы: wait, notify_one и notify_all.

GCC 😊, CLANG 😐, VS 😊

Заключение


Рассказ о фичах C++ 20 окончен. Мы рассмотрели все основные изменения, хотя на самом деле в Стандарте ещё много разного и интересного. Но это уже технические особенности, которые не обязательно знать каждому профессиональному программисту.

Вполне возможно, я что-то упустил — тогда добро пожаловать в комментарии.
C++ на этом не останавливается, Комитет по стандартизации активно работает. Уже было заседание, посвящённое Стандарту 2023 года. То, что в него уже включили, нельзя назвать киллер-фичами. Но ожидания от нового Стандарта большие. Например, в него войдут контракты и полноценная поддержка корутин.

На Хабре круто встретили предыдущие части. В статьях про Модули и Ranges развернулись особо оживлённые дискуссии. Будет здорово, если вы расскажете о своих впечатлениях от C++20 и ожиданиях от новых стандартов. Например, какую фичу C++20 вы считаете самой крутой и важной? Чего больше всего ждёте от будущего языка? Приветствуются также дополнения, уточнения, исправления — наверняка что-то важное я упомянуть забыл.

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

Поживем — увидим.

Опрос


Читателям Хабра, в отличие от слушателей вебинара, дадим возможность оценить нововведения.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Оцените другие фичи ядра и стандартной библиотеки:
68.38% Суперфичи 93
11.03% Так себе фичи 15
20.59% Пока неясно 28
Проголосовали 136 пользователей. Воздержались 19 пользователей.
Теги:
Хабы:
+39
Комментарии 52
Комментарии Комментарии 52

Публикации

Информация

Сайт
practicum.yandex.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
Ира Ко