Interested Article - C++26

C++26 или C++2c (латиницей), или Си++26 (кириллицей) — ожидаемый стандарт языка программирования C++ . Разработка началась сразу же после того, как в феврале 2023 года зафиксировали Си++23 .

Название

Си++0x должен был приблизить Си++ к современным языкам, непрерывно разрабатываемым под руководством единоличника . (Си++ разрабатывается комитетом и есть много реализаций — в отличие от, например, Python .) Но стандарт запоздал, и с версии 14 новый язык выпускают не «когда готово», а раз в три года, при этом последний год — только доводка. Потому ещё до окончания работы над версией 23 стандарт называли «Си++26».

Заседания

  1. 12…16 июня 2023, Варна (Болгария) — первое после пандемии КОВИДа очное заседание.
  2. 6…11 ноября 2023, Каилуа-Кона (Гавайи, США)
  3. Март 2024, Токио (Япония) — ожидается .

Запрещены и удалены

Удалены

  • Любые операции между enum и дробным; enum и другим enum. Ошибкоопасное наследие Си. Запрещены в Си++20, операция «звездолёт» никогда не разрешалась . Использовать явное преобразование типов. Может бросить тень на совместимость с Си, обходится легко: + C1 + C2 .
  • Весь заголовок < codecvt > — нет обработки ошибок . При преобразовании кодировок, даже разных форматов сериализации Юникода вроде UTF-8 16 , в зависимости от того, откуда источник взялся, какой вред может нанести и что лучше для пользователя, возможно: объявить общую аварию программы, сообщить о плохих данных, опустить неудачные последовательности, поставить на их месте U+FFFD � replacement character или что-то ещё. Запрещён в Си++17. Использовать внешние, более управляемые функции.
  • allocator < T >:: is_always_equal . Ошибкоопасен при наследовании от аллокатора, в котором этот is_always_equal есть. Запрещён в Си++20, для проверки возможностей аллокатора использовать allocator_traits . Использовать в собственных аллокаторах, когда это действительно играет роль.
  • string . reserve () без параметров, эквивалентный reserve ( 0 ) . Со старым API строк (Си++98…17) использовалось как shrink_to_fit , им же и заменено. В Си++20 reserve больше не укорачивает строку, а данную перегрузку запретили.

Язык

Разные изменения в языке

  • Параметром-значением в шаблонах ( non-type template parameter ) может стать и вызов конструктора. Указано, когда такой вызов возможен, а когда нет .
  • Получение i-го объекта пакета параметров как void f ( T && ... t ) { g ( std :: forward < T ...[ 0 ] > ( t ...[ 0 ])); } . Формально это несколько бьёт по имеющемуся коду: void f ( T ...[ 0 ]){} представляло собой параметры без имён — пакет массивов, но по факту даже не компилировалось в MSVC и G++. C# и D поддерживают и i-й параметр с конца, но отрицательные числа для этого ошибкоопасны, а более сложный синтаксис решено не просить. Главная причина — ускорение компиляции: пакетные шаблоны часто исполняются за O ( n ²) и с глубокой вложенностью.

Конструируемые строки в static_assert

Для начала придумали понятие «невычисляемая строка» ( unevaluated string ): закавыченная строка, значение которой не проходит в скомпилированную программу, а нужно только компилятору. Они являются частью _Pragma , asm , [[ nodiscard ]] … — и, конечно, static_assert . Им запрещается иметь префикс кодировки.

Впоследствии позволили в static_assert любую константно вычисляемую строку :

// Было
template <typename T, auto Expected, unsigned long Size = sizeof(T)>
constexpr bool ensure_size() {
  static_assert(sizeof(T) == Expected, "Unexpected sizeof");
  return true;
}
static_assert(ensure_size<S, 1>());
// Остаётся надеяться, что компилятор напишет, что дело было в ensure_size<int, 1, 4>
// Стало
static_assert(sizeof(S) == 1,
    std::format("Unexpected sizeof: expected 1, got {}", sizeof(S));
// Unexpected sizeof: expected 1, got 4

constexpr format намеренно не внесён, но его прообраз, библиотека libfmt, уже способна на constexpr.

Имя _ может повторяться

auto [ x , y , _ ] = f (); — давно устоявшаяся манера программирования, когда функция возвращает три поля, а нужны два, особенно если возвращается неговорящий тип вроде tuple . Второй вариант — когда временный объект не годится, а имя не важно: например, захват мьютекса lock_guard _ ( someMutex ) . На случай, когда таких подчерков несколько, идиому расширили:

namespace a {
  auto _ = f();
  auto _ = f(); // Остаётся ошибка: с глобальными переменными не работает
}
int _;
void f() {
  using ::_;   // Остаётся OK, добавление в пространство имён постороннего символа
  auto _ = 42; // Теперь OK
  using ::_;   // Остаётся ошибка: using _ разрешено только до локальной _
  auto _ = 0;  // Теперь OK
  static int _; // Остаётся ошибка: со статическими переменными не работает
  {
    auto _ = 1;       // Остаётся OK, замещение
    assert( _ == 1 ); // Остаётся OK, имеем дело с замещённой переменной
  }
  assert( _ == 42 );  // Ошибка: которая из двух?
}
enum MmapBits {
  Shared,
  Private,
  _,
  _,   // OK: пропустить два
  Fixed,
  Rename,
};

Для функций, типов, using и концепций новый механизм бесполезен: этим объектам либо нужно говорящее имя, либо Си++ уже даёт подходящие механизмы вроде безымянных типов .

Расширен constexpr

  • Преобразование указателей в void * , а потом обратно в свой тип . Преобразование в посторонние типы неконстантно. Используется для так называемого стирания типа — при выполнении информация хранится в переменной общего типа, но её обработка выстраивается так, что все преобразования в исходный тип верны. (Так устроены, например, обобщённые типы Java .) В CLang механизм уже есть (потребовался для выделения памяти) и вынести наружу ничего не стоит, G++ и не видят препятствий. По заявлениям Г. Саттера , это шаг к constexpr format .

Редакционные правки

  • Стандартизирован лексический анализатор: сращиванием строк текста через \⤶ и склеиванием лексем через препроцессорное ## можно получить имя символа; переводы строк внутри закавыченной строки запрещены. Это статус-кво , поддерживаемый G++, CLang и .
  • Некодируемые строковые литералы (например, из-за отсутствия конкретного символа в кодировке исполнения) запрещены .
  • Уточнены правила игнорирования стандартных атрибутов :
    • Атрибут должен быть синтаксически корректным по правилам текущего Си++ — иначе ошибка. (Уже в Си++23 и только добавлено примечание.)
    • Убирание атрибута из корректной программы может менять её внешнее поведение, но не может придумывать новое — лишь ограничивать до одного из допустимых вариантов, когда атрибут есть. (Также в Си++23.)
    • Псевдофункция препроцессора __has_cpp_attribute должна проверять, реагирует ли компилятор на данный атрибут (а не разбирает ли) — а если разбирает, но не реагирует, атрибут бесполезен и макросы совместимости должны развёртываться в компиляторозависимые функции вроде __builtin_assume . (А это новое правило.)
  • Экземпляры initializer_list по возможности хранятся в , желательно неизменяемом .
  • Требования к generate_canonical переписаны так, чтобы сохранялись статистические свойства на всём диапазоне [0,1) — и результирующее число никогда из-за недостатков дробной арифметики не стало бы единицей .

Гармонизация с Си

  • В набор символов внесены остатки печатного ASCII @$` , которые когда-нибудь на что-нибудь пригодятся . Ранее в Си23 добавили @$ , в первую очередь из-за EBCDIC , где в разных вариантах «собака» в разных местах и надо её идентифицировать и корректно перекодировать .
  • Выкинут strtok из автономной библиотеки вслед за Си — содержит внутреннее состояние. Большинство реализаций используют потоколокальные переменные, которые в автономной среде могут отсутствовать.
  • Переписан макрос assert , чтобы лучше поддерживались шаблоны и многомерная индексация, коих просто не существовало на момент появления препроцессора Си .

Библиотека

Разные изменения в библиотеке

  • Простейшая библиотека идентификации кодировки исполнения .
  • Получение системного дескриптора из fstream . Может использоваться в высоконадёжном программировании, когда надо гарантированно записать данные на диск .
  • Поддержка отладчика. Новый заголовочный файл < debugging > с тремя функциями: breakpoint () , breakpoint_if_debugging () , bool is_debugger_present () .

Автономная библиотека

Автономная ( freestanding ) библиотека не полагается на системные вызовы (даже выделение памяти), может быть написана даже на чистом Си++ и потому полностью кроссплатформенна.

  • Возможен (не обязателен) operator new , возвращающий nullptr , приводящий к системной аварии или делающий что угодно по желанию реализатора. Добавлен макрос __cpp_lib_has_default_operator_new , проверяющий, возможно ли выделение памяти — например, вместо динамического std::vector могут использоваться массивы ограниченного размера .
  • Множество функций Си, включая строковые и математические, а также < charconv > и char_traits .
  • algorithm , array , optional , variant , string_view . Переписаны монадные функции optional так, чтобы не ссылались на неавтономный (выбрасывающий исключения) value .
  • expected , span , mdspan .
  • Выкинут strtok вслед за Си — использует потоколокальные переменные, которые в автономной среде могут отсутствовать.

Новые constexpr

  • Устойчивая сортировка .
  • consteval bool is_within_lifetime ( & union_ . field ) — «волшебная» (реализованная внутри компилятора) функция, проверяющая, держит ли union то или иное поле . Ведь при компиляции union становится помеченным на манер variant , а доступ к другому полю отключает расчёт при компиляции. Используется для экстремальной оптимизации по памяти с сохранением константности — например, для однобайтового optional < bool > .
  • Больше математических функций, включая комплексные .

Перевод данных в строку и наоборот

  • from_chars_result получил operator bool — проверку кода ошибки.
  • to_string для дробных выдаёт то же, что и format ( "{}" , x ) — в компактном нелокализованном виде. Ранее он был унифицирован с printf ( "%f" , x ) , то есть обращался к глобальной локали (ненадёжно, да и вычисление нужных параметров локали затратно) и плохо работал со слишком большими/малыми числами .
  • stringstream можно инициализировать строками string_view .
  • То же самое с bitset .

format

  • Унифицировано форматирование указателей .
  • Параметры ширины теперь также проверяются при компиляции .
  • Форматирование строк, заранее не известных: std :: vformat ( str , std :: make_format_args ( path . string ())); std :: format ( std :: runtime_format ( str ), path . string ()); . Первое предназначено для писателей своих обёрток над форматированием вроде doLog ( str , args ...) , а не для конечных пользователей, и склонно к ошибкам: make_format_args содержит string_view, и если его вытащить в отдельную переменную, string_view будет жить дольше, чем временная строка. Для надёжности тонкая обёртка runtime_format_string принимается только по временной ссылке .
  • В само́м make_format_args избавились от std::forward и временных объектов , делая форматирование более устойчивым к висячим ссылкам .
  • Серьёзная ошибка, ранее случившаяся в fmt: кодовые единицы char, будучи отформатированы как числа или в «широком» контексте, выдавали зависящий от реализации вид . Теперь char, отформатированный как число, будет unsigned; отформатированный как символ в широком контексте — символом с кодом 0…255.

Изменения в функциональном программировании

  • Добавлена copyable_function , построенная по принципу новой move_only_function (Си++23) и значительно более лёгкая , чем function (Си++11). Последнюю всё-таки решили не запрещать .
  • Добавлена совсем лёгкая function_ref , не инкапсулирующая вызываемый объект, а просто ссылающаяся на него .
  • Добавлен облегчённый шаблонный карринг через bind_front , если вызываемый объект (например, слот Qt ) вычисляется раз и навсегда при компиляции .
  • This-параметры из Си++23 позволили внести одну из перегрузок visit внутрь variant .

Хранение данных

  • Добавлен hash для календарных типов .
  • Добавлен weak_ptr . owner_hash и несколько других подобных функций .
  • Закончен разнородный поиск в [ unordered_ ] set / map : добавлены шаблонные insert , insert_or_assign , try_emplace , operator [] , bucket . Разнородный поиск начат в Си++14, и позволяет хранить с «тяжёлыми» ключами ( string ), а искать по «лёгким» ( или даже const char * ).

Диапазоны и другие представления данных

  • Переписан projected (внутренний тип библиотеки диапазонов), лучше работающий с указателями на недоопределённые классы ( class Opaque ; ). Многие из функций диапазонов не работали там, где работал «голый» STL .
  • Комплексным числам добавлено get < 0 > и < 1 > , как обычным кортежам ( tuples ) .
  • basic_const_iterator можно получить из неконстантного собрата .

span и mdspan

  • Функция submdspan , производящая многомерных массивов. На выходе получается mdspan (Си++23), возможно, с нестандартным типом внутри .
  • Конструктор span ( initalizer_list ) , не требующий промежуточного объекта вроде массива .
  • span . at ( i ) , выкидывающий аварию .

Примитивы неблокирующей синхронизации

Read-copy-update

Объект хранится в динамической памяти. Как только этот объект изменили, создают новый такой же, а старый, когда можно, удаляют .

// Было — блокирующая версия
Data* data_;
std::shared_mutex m_;

template <typename Func>
auto reader_op(Func fn) {
  std::shared_lock<std::shared_mutex> l(m_);
  Data* p = data_;
  return fn(p);
}

void update(Data* newdata) {
  Data* olddata;
  { std::unique_lock<std::shared_mutex> wlock(m_);
    olddata = std::exchange(data_, newdata);
  }
  delete olddata;
}
// Стало — не блокируются только читатели
std::atomic<Data*> data_;

template <typename Func>
auto reader_op(Func fn) {
  std::scoped_lock l(std::rcu_default_domain());
  Data* p = data_;
  return fn(p);
}

void update(Data* newdata) {
  Data* olddata = data_.exchange(newdata);
  std::rcu_synchronize();
  delete olddata;
}

Главный недостаток идиомы read-copy-update в данном исполнении — не ждут только читатели, писатель может надолго «зависать». Это «зависание» означает, что другие читатели работают и держат объект, но не всегда допустимо.

Hazard pointer дополнительно следит, какие потоки пользуются тем или иным объектом, и как только объект перестаёт использоваться, он исчезает .

Идиома похожа на подсчёт ссылок , но подсчитывает только локальные ссылки из функций доступа — а не глобальные ссылки между объектами. Это позволяет циклические ссылки без слежения, чей «ранг» выше (от «контейнеров» к «содержимому» — shared_ptr , в прочие стороны — weak_ptr ), а также без присущего shared / weak_ptr управляющего объекта , исчезающего, когда исчезнет последний слабый указатель .

Система сделана беспрепятственной по записи ценой повышенного расхода памяти: read-copy-update хранит одно поколение старых данных, а hazard pointer — сколько угодно .

Поскольку G++ всё ещё держит совместимость двоичных интерфейсов , на будущие дополнения оставили 4/8 байтов на объект.

(Старая блокирующая версия — та же)
// Стало — не блокируется и писатель
struct Data : std::hazard_pointer_obj_base<Data> {}
std::atomic<Data*> pdata_;

template <typename Func>
auto reader_op(Func userFn) {
  std::hazard_pointer h = std::make_hazard_pointer();
  Data* p = h.protect(pdata_);
  return userFn(p);
}

void writer(Data* newdata) {
  Data* old = pdata_.exchange(newdata);
  old->retire();
}

Математика

Арифметика с насыщением (упором в край)

Стандартная работа беззнаковых типов — арифметика остатков : при переходе через значение превращается в 0. Знаковые — зависят от реализации . Но это не всегда нужно: например, может означать «сколько угодно» и прибавление к нему единицы должно оставлять . Никакой защиты от дурака нет .

#include <numeric>

// Считаем, что у нас 8-битный char и отрицательные в дополнительном коде
int x1 = add_sat(3, 4);               // 7
int x2 = sub_sat(INT_MIN, 1);         // INT_MIN
unsigned char x3 = add_sat(255, 4);   // 3!! — работа в int и преобразование 259 → 3
unsigned char x4 = add_sat<unsigned char>(255, 4);   // 255
unsigned char x5 = add_sat(252, x3);  // Ошибка, нет нужной перегрузки
unsigned char x6 = add_sat<unsigned char>(251, x2);  // 251!! — преобразование INT_MIN → 0

Заполненная линейная алгебра

Добавились BLAS -подобные алгоритмы линейной алгебры для заполненных (большей частью ненулевых) векторов и матриц . Мотивация:

  • Комитет Си++ сам поставил линейную алгебру приоритетом.
  • Си++ — стандартная платформа для наукоёмкого ПО, которому линейная алгебра более чем нужна.
  • Это как сортировка массива : примитивные алгоритмы медленные, а самые быстрые реализации можно получить аппаратно-специфичными улучшениями.
  • В стандарте Си++ и так много разной математики — и умножение матриц не менее важно, чем функции Бесселя .
  • BLAS — известный стандарт линейной алгебры, мало менявшийся с годами.
  • Это такой же путь к интеграции в Си++ сторонних стандартов, как Юникод (идёт работа) и часовые пояса.

Конструкция полностью шаблонная и на mdspan .

Пока вне рассмотрения: расширенные функции BLAS/ LAPACK , разреженная алгебра, расчёты повышенной точности, тензоры , параллельная работа, перегрузка операций ±. Последняя — из-за неоднозначности (есть несколько типов умножения векторов), данные могут быть в одном типе, а работа в другом, и из-за больших объёмов памяти промежуточные буфера часто используются повторно.

Нет даже решения заполненных СЛАУ . Вот одна из стандартных функций — решение треугольной СЛАУ на месте.

template<in-matrix InMat,
         class Triangle,
         class DiagonalStorage,
         inout-vector InOutVec>
void triangular_matrix_vector_solve(
  InMat A,
  Triangle t,
  DiagonalStorage d,
  InOutVec b);

Здесь пустой тип-тэг Triangle показывает, каким треугольником собрана матрица, верхним или нижним. Аналогичный тэг DiagonalStorage — что представляет собой диагональ матрицы A: явные значения или неявные единицы. В векторе b изначально правая часть системы, в результате расчёта будет решение.

Ожидаются, но не одобрены

  • Гармонизация с Си:
    • #embed — инициализация массива данных двоичным файлом , прошла в Си23.
  • Примитивы хранения данных:
    • inplace_vector — массив изменяемой ограниченной длины на основе обычного массива .
    • Улей — специализированный менеджер памяти для однотипных данных, используемый в играх и скоростной торговле. Никогда не перемещает, объект вставляется в случайное место, относительно быстры операции «проход», «добавление» и «удаление» .
    • path_view , аналог string_view для путей .
  • Многозадачность:
  • Прочее:
    • Улучшения в библиотеке диапазонов .
    • Замена integral_constant на более простой в использовании constexpr_v .
    • Улучшение рефлексии при компиляции .

Будут неизвестно когда

Крупные важные функции. Их давно просят, но нет гарантии, что будут готовы к сроку Си++26 .

  • Библиотечная поддержка сопрограмм (языковая есть в Си++20)
  • Сеть — не удалось сделать модульный подход
  • Контрактное программирование — уточнение условий на параметры функций
  • Некое pattern matching — возможно, описываемое в некоторых статьях гипотетическое ключевое слово inspect , аналог switch , действующий даже на разные объектные подтипы и разные шаблоны строк

Комментарии

  1. Здесь и далее «лёгкий/тяжёлый» — по системным ресурсам ( процессорному коду , расходу памяти и т. д.), «простой/сложный» — по работе программиста, «простейший» — по функциональности.

Примечания

  1. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  2. . Дата обращения: 16 ноября 2023. 16 ноября 2023 года.
  3. . Дата обращения: 10 августа 2023. 10 августа 2023 года.
  4. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  5. . Дата обращения: 29 ноября 2023. 15 ноября 2023 года.
  6. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  7. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  8. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  9. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  10. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  11. . Дата обращения: 29 августа 2023. 29 августа 2023 года.
  12. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  13. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  14. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  15. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  16. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  17. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  18. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  19. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  20. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  21. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  22. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  23. . Дата обращения: 17 ноября 2023. 17 ноября 2023 года.
  24. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  25. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  26. . Дата обращения: 14 сентября 2023. 5 сентября 2023 года.
  27. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  28. от 28 июля 2023 на Wayback Machine .
  29. . Дата обращения: 8 августа 2023. 10 августа 2023 года.
  30. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  31. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  32. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  33. . Дата обращения: 31 июля 2023. 21 июля 2023 года.
  34. . Дата обращения: 31 июля 2023. 30 июля 2023 года.
  35. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  36. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  37. . Дата обращения: 31 июля 2023. 21 июля 2023 года.
  38. . Дата обращения: 31 июля 2023. 21 июля 2023 года.
  39. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  40. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  41. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  42. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  43. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  44. . Дата обращения: 31 июля 2023. 21 июля 2023 года.
  45. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  46. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  47. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  48. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  49. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  50. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  51. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  52. . Дата обращения: 14 ноября 2023. 10 октября 2023 года.
  53. . Дата обращения: 14 ноября 2023. 15 ноября 2023 года.
  54. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  55. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  56. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  57. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  58. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  59. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  60. . Дата обращения: 14 ноября 2023. 14 ноября 2023 года.
  61. . Дата обращения: 14 ноября 2023. 5 ноября 2023 года.
  62. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  63. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  64. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  65. . Дата обращения: 31 июля 2023. 21 июля 2023 года.
  66. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  67. . Дата обращения: 31 июля 2023. 24 сентября 2023 года.
  68. . Дата обращения: 9 августа 2023. 11 августа 2023 года.
  69. . Дата обращения: 31 июля 2023. 31 июля 2023 года.
  70. . Дата обращения: 8 августа 2023. 10 августа 2023 года.
  71. . Дата обращения: 8 августа 2023. 10 августа 2023 года.
Источник —

Same as C++26