25 февраля автор курса «Разработчик C++» в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ — Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.
При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся на почти 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:
- Модули и краткая история C++.
- Операция «космический корабль».
- Концепты.
- Ranges.
- Корутины.
- Другие фичи ядра и стандартной библиотеки. Заключение.
Это шестая, заключительная часть. Она рассказывает о других нововведениях ядра и стандартной библиотеки, добавленных Стандартом 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 😊
Лямбды
Раньше в лямбда-функциях было три вида скобок. Теперь их может быть четыре — стало возможным писать явные параметры для лямбда-функций:
- квадратные скобки для переменных связывания,
- угловые скобки для шаблонных параметров,
- круглые скобки для списка аргументов,
- фигурные скобки для тела функции.
Порядок важен: если его перепутать, будет ошибка. Если шаблонных параметров нет, то угловые скобки не пишите, потому что пустыми они быть не могут. Кстати, эта фича уже нормально поддерживается во всех компиляторах.
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++ уже есть некоторые предложения по поводу того, как она может выглядеть.
Поживем — увидим.
Опрос
Читателям Хабра, в отличие от слушателей вебинара, дадим возможность оценить нововведения.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Оцените другие фичи ядра и стандартной библиотеки:
-
77,8%Суперфичи7
-
11,1%Так себе фичи1
-
11,1%Пока неясно1