Это шестая и последняя часть. В нем рассказывается о других новшествах ядра и стандартной библиотеки, добавленных стандартом C++20.
- Модули и краткая история C++.
- Операция «Космический корабль.
- понятия.
- диапазоны.
- Корутины.
- Другие функции ядра и стандартной библиотеки. Заключение.
Другие основные функции
Я рассказал о самых значимых новшествах Стандарта, но это лишь капля в море 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, constructs 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
.
Время компиляции
Появился новый тип переменных — 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
, которую можно вызывать как во время выполнения, так и во время компиляции, эти функции нельзя вызывать даже во время выполнения, будет ошибка.
consteval int sqr(int n) { return n * n; } constexpr int r = sqr(100); // OK int x = 100; int r2 = sqr(x); // <--
error: result is not a constantconsteval int sqrsqr(int n) { return sqr(sqr(n)); } constexpr int dblsqr(int n) { return 2*sqr(n); } // <-- error, // constexpr may be computed at run-time
Многих это может сбить с толку. Зачем тебе еще один constexpr
? Но представьте действие, которое только имеет смысл во время компиляции. Например, вы помещаете файлы ресурсов в проект и читаете их во время компиляции. Вы не можете сделать это прямо сейчас, так что просто представьте. Во время выполнения эти ресурсы больше не будут существовать.
GCC 😊, CLANG 😐, VS 😊
В C++20 constexpr
значительно расширился, теперь внутри него можно вызывать даже виртуальные функции! И, что особенно примечательно, многие контейнеры стали поддерживать constexpr
.
Новые синтаксические конструкции
Мы ждали их с 1999 года. Все это время программисты на чистом C дразнили нас тем, что они могут их использовать, а мы нет. Вы понимаете, о чем я говорю? Конечно, это назначенные инициализаторы!
Теперь при построении объекта структуры можно явно написать, какому полю какое значение присваивается. Это очень круто. Ведь по структурам с 10–15 полями практически невозможно понять, что чему соответствует. Я видел такие конструкции. Кроме того, поля можно пропускать. Но порядок изменить нельзя. И вы не можете сделать еще несколько вещей, которые можно сделать в C. Они показаны в примере.
struct A { int x; int y; int z; }; A b{ .x = 1, .z = 2 }; A a{ .y = 2, .x = 1 }; // <-- error, out of order
struct A { int x, y; }; struct B { struct A a; }; int arr[3] = {[1] = 5}; // <-- error – array struct B b = {.a.x = 0}; // <-- error – nested struct A a = {.x = 1, 2}; // <-- error – two kinds of initializers
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 class fruit { orange, apple }; enum class color { red, orange }; void f() { using enum fruit; // OK using enum color; // <--
error - conflict}
GCC 😊, CLANG 😔, VS 😊
Другой
- Операция с запятой внутри
[]
объявлена устаревшей. Намек на что-то интересное в будущем.
GCC 😊, CLANG 😊, VS 😊
- Запрет некоторых действий с volatile переменными. Эти операции также объявлены устаревшими. Переменные volatile теперь часто используются не по назначению. Судя по всему, комитет решил с этим бороться.
GCC 😊, CLANG 😊, VS 😊
- Агрегатные типы можно инициализировать простыми круглыми скобками — раньше разрешались только фигурные скобки.
GCC 😊, CLANG 😔, VS 😊
- Появился оператор destroy, который не вызывает деструктор, а просто освобождает память.
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'; }
// output: 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}", "NY", "Hello"); // s2 == "Hello from NY"
int width = 10; int precision = 3; auto s3 = format("{0:{1}.{2}f}", 12.345678, width, precision); // s3 == " 12.346"
Он обещает быть намного более производительным, чем потоковая передача с stringstream
, но не без проблем. Первая проблема: формат не проверяет все ошибки и вообще не анализирует формат во время компиляции на данном этапе. Это очень расстраивает.
Вторая проблема заключается в том, что он еще не реализован ни в одной стандартной библиотеке, поэтому его нельзя проверить на практике.
GCC 😔, CLANG 😔, VS 😊
- Отличные новости: в Standard теперь
pi
. Кроме него добавляются обратное π, число Эйлера, логарифмы некоторых чисел, корни и обратные корни, корень из π вместе с обратным, постоянная Эйлера-Маскерони и золотое сечение. Все они доступны при включении<numbers>
в пространство именstd::numbers
.
GCC 😊, CLANG 😊, VS 😊
- Новые алгоритмы:
shift_left
иshift_right
. Они сдвигают элементы диапазона на заданное количество позиций. При этом скручивания не происходит: элементы, выходящие на край, не телепортируются на другой конец, а уничтожаются. С другого конца появляются пустые элементы.
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 since negative numbers are not representable in size_tstd::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
создает массив из массива C или строкового литерала.
GCC 😊, CLANG 😊, VS 😊
- В заголовочный файл версии добавлены макросы для проверки наличия функций стандартной библиотеки. Для функций ядра уже были макросы, но теперь они расширены до стандартной библиотеки.
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.count(x) > 0
можно написатьmy_map.contains(x)
и сразу всем понятно, что нужно проверить наличие ключа;- версии функции
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);
}
Если указатель окажется невыровненным на самом деле, добро пожаловать, поведение undefined. Выравнивание указателя позволит компилятору генерировать более эффективный код векторизации.
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-битного целого числа. Функция cmp_greater
обойдет эту функцию и выполнит реальное математическое сравнение:
std::cmp_greater(-1, 0u); // false
GCC 😊, CLANG 😊, VS 😊
- Еще одно нововведение —
source_location
. Это класс, который позволит вам заменить макросы__LINE__
и__FILE__
, используемые при ведении журнала. Статический текущий метод этого класса вернет объектsource_location
, содержащий строку и имя файла, в котором был вызван этот метод. Вот я задал вопрос. Какое число выведет эта функция?
void
log(const std::source_location& location = std::source_location::current()) { std::cout << location.line(); }int
main() { log(); }
Есть два варианта:
- цифра 2, которая соответствует строке, где написано
source_location
; - число 7, соответствующее строке, где функция
log
вызывается.
Правильный ответ — 7, где вызывается функция, хотя это не кажется очевидным. Но именно из-за этого обстоятельства source_location
можно использовать как замену макросам для логирования. Потому что когда мы логируем, нас интересует не то, где написана функция, а то, откуда она вызывается.
GCC 😊, CLANG 😔, VS 😊
- Закончим обзор на радостной ноте: C++ значительно упростил многопоточное программирование.
- Новый класс
counting_semaphore
- мы ждем, пока определенное количество раз семафор не будет разблокирован. - Классы
latch
иbarrier
блокируются до тех пор, пока определенное количество потоков не достигнет определенного места. - Новый тип потока:
jthread
. Он выполняет соединение в деструкторе без сбоя вашей программы. Такжеjthread
поддерживает флаг отмены, через который удобно прерывать выполнение потока —stop_token
. Есть несколько новых классов, связанных с этим флагом. - Еще один новый класс
atomic_ref
— это специальная ссылка, которая блокирует операции других потоков с объектом. - Возможности атома были значительно расширены. Теперь он поддерживает числа с плавающей запятой и интеллектуальные указатели, а также новые методы:
wait
,notify_one
иnotify_all
.
GCC 😊, CLANG 😐, VS 😊
Заключение
Рассказ о фичах C++20 окончен. Мы рассмотрели все основные изменения, хотя на самом деле в Стандарте еще много разного и интересного. Но это уже технические особенности, знать которые нужно далеко не каждому профессиональному программисту.
Вполне возможно, что я что-то упустил — вы можете сообщить мне об этом в комментариях.
C++ не останавливается на достигнутом, комитет по стандартам активно работает. Уже состоялось совещание, посвященное Стандарту 2023 года. То, что в него уже вошло, нельзя назвать киллер-фичами. Но ожидания от нового стандарта высоки. Например, он будет включать в себя контракты и полную поддержку сопрограмм.
Будет здорово, если вы расскажете о своих впечатлениях от C++20 и ожиданиях от новых стандартов. Например, какую функцию C++20 вы считаете самой крутой и важной? Чего вы больше всего ожидаете от будущего языка? Дополнения, уточнения, исправления тоже приветствуются — наверное забыл упомянуть что-то важное.
Лично я больше всего жду от новых Стандартов добавления рефлексии — способности программы анализировать и изменять себя. В контексте C++ уже есть некоторые предложения о том, как это могло бы выглядеть.
Ждать и смотреть.