C++20

C++20 — название стандарта ISO/IEC языка программирования C++. Спецификация опубликована в декабре 2020 года[1].

Комитет по стандартам C++ начал планировать C++20 в июле 2017 года[2]. C++20 является преемником C++17.

Константа __cplusplus увеличилась до 202002L.

Краткий список

В стандарт попали следующие добавления в июле 2017[3]
  • концепции[4]
  • назначенные инициализаторы (designated initializers_ (основанные на стандарте C99)[5]
  • [=, this] как лямбда-захват (lambda capture)[6]
  • шаблонные параметры списка в ламбда-выражениях[7]
В стандарт попали следующие добавления в ноябре 2017[8][9]
  • инициализация дополнительной переменной в пределах диапазона в цикле for
  • лямбды в невычисленных контекстах[10][11]
  • конструируемые и назначаемые лямбды без сохранения по умолчанию[10][12]
  • init-capture[10][13]
  • строковые литералы как параметры шаблона[10][14]
В стандарт попали следующие добавления в июне 2018[15]
  • контракты[16]
Добавления ноября 2018[17]
  • concept terse syntax[18]
  • уточнения объекта контракта (контроль доступа в контрактах)[19]
  • пересмотренная модель памяти[20]

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

Запрещены операции с volatile

Модификатор volatile явно машинозависимый — для общения с аппаратурой. Так что непонятно, какова семантика той или иной операции и сколько будет обращений к памяти. Для межпоточной синхронизации лучше использовать atomic.

Запрещены следующие операции с volatile-переменными[21]:

  • операции ++, −− со стандартными типами;
  • цепочки присваиваний;
  • функции с модификатором volatile;
  • все функции STL, связанные с volatile, кроме некоторых вроде remove_volatile;

Для atomic добавлены дополнительные функции, компенсирующие то, что запретили.

Удалена агрегатная инициализация при наличии пользовательского конструктора

В предыдущих стандартах агрегатная инициализация разрешалась, если конструктор был помечен как default или delete, что вводило пользователей в заблуждение.

struct X {
  int a = 0;  
  X() = default;
};

X x { 5 };  // в С++20 это вызывает ошибку компиляции
            // no matching constructor for initialization of 'X'

Удалены запреты из C++17

Удалены редкие возможности стандартной библиотеки, запрещённые в C++17:[22][23][24]

  • allocator<void> — оказался невостребованным;
  • часть функций allocator — дублируется шаблоном allocator_traits;
  • raw_storage_iterator — не вызывает конструкторов и потому ограничен по применению;
  • get_temporary_buffer — имеет неочевидные подводные камни;
  • is_literal_type — бесполезен для обобщённого кода;
  • shared_ptr::unique() — из-за ненадёжности в многопоточной среде; если очень надо, используйте use_count;
  • result_of — заменён на invoke_result;
  • uncaught_exception() — заменён на uncaught_exceptions.
  • <ccomplex>, <ciso646>, <cstdalign>, <cstdbool>, <ctgmath> — не имеют смысла в Си++. <complex.h> и прочие оставили для совместимости с Си.

Из языка удалили ремарку throw(), которую ещё в Си++11 заменили на noexcept. Если нужна совместимость с Си++03, в заголовках совместимости нужно прописывать что-то вроде

#if __cplusplus < 201103L
  #define noexcept throw()
#endif

Оставили:

  • <codecvt> — на поверку работал очень плохо, комитет призвал пользоваться специализированными библиотеками.
  • iterator — проще писать итераторы с нуля, чем основываться на нём.
  • потоки char* — непонятно, что взамен.
  • неявное создание операции «присвоить», если есть конструктор копирования и деструктор (а также конструктора копирования, если есть присваивание или деструктор) — библиотека всё ещё полагается на это поведение.

Прочие запреты из языка

  • Неявный перехват *this в лямбда-функциях [](){ std::cout << myField; } — из-за неясной семантики. Существует [this](){ std::cout << myField; } для перехвата по указателю и [*this](){ std::cout << myField; } для перехвата по значению.
  • Операция «запятая» в индексах a[b,c] для любых a, b и c — из-за неочевидного поведения и желания создать новый синтаксис для многомерных массивов[25]. Если очень нужно, пишите a[(b,c)].
  • Неявные преобразования в перечисляемый тип — для более прогнозируемого поведения новой операции «звездолёт» (<=>, трёхзначное сравнение).
  • Сравнение двух массивов — для более прогнозируемого поведения новой операции «звездолёт» (<=>, трёхзначное сравнение). Хотя бы один надо преобразовать в указатель.

Прочие запреты из библиотеки

  • is_pod — лучше использовать конкретные свойства типа: тривиально строится, тривиально уничтожается и т. д. Если очень надо, эквивалентно is_trivial && is_standard_layout.
  • std::rel_ops — новая операция «звездолёт» делает это лучше.
  • атомарные возможности shared_ptr — непонятно, как работать с указателем, атомарно или нет. Лучше это определить системой типов, atomic<shared_ptr>.
  • string::capacity() — теперь решили, что reserve не будет уменьшать ёмкость.
  • filesystem::u8path — теперь u8string отличается от string.
  • ATOMIC_FLAG_INIT, atomic_init, ATOMIC_VAR_INIT — теперь это делает шаблонный конструктор atomic.

Язык

Мелкие изменения

  • Добавлен беззнаковый тип char8_t, способный содержать единицы UTF-8.
  • using EnumClass, позволяющий сделать код в ключевых местах менее загромождённым.

Модули

Директива компилятора #include в своё время была удобным механизмом Си, который, был, по сути, кроссплатформенным ассемблером, «паразитировавшим» на ассемблерных утилитах — линкере и библиотекаре. Но с расширением проектов квадратично повышалось время их компиляции: увеличивалось как количество единиц трансляции, так и количество подключённых к ним заголовков. Механизм модулей был долгим объектом споров ещё со времён Си++11.

В Си++20 он вошёл в таком виде[26]:

// helloworld.cpp
export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
export void hello() {      // export declaration
    std::cout << "Hello world!\n";
}

Сопрограммы

Сопрограмма — это специальная бесстековая функция, которая может приостановить своё исполнение, пока выполняется другая функция[27]. Состояние сопрограммы хранится в динамической памяти (кроме случаев, когда оптимизатору удалось избавиться от выделения). Выглядит как обычная функция, но содержит особые сопрограммные ключевые слова co_*.

task<> tcp_echo_server() {
  char data[1024];
  for (;;) {
    size_t n = co_await socket.async_read_some(buffer(data));
    co_await async_write(socket, buffer(data, n));
  }
}

Физически сопрограмма — это функция, возвращающая свежесозданный объект-обещание. Каждый раз, когда пользователь делает что-то с объектом-обещанием, управление передаётся коду сопрограммы. В библиотеке доступны несколько стандартных обещаний — например, lazy<T> обеспечивает ленивое вычисление.

typename объявлен излишним там, где допустим только тип

В некоторых местах шаблонов слово typename (объяснение, что Object::Thing — это тип, а не функция) больше не требуется[28]. К таким местам относятся…

  • тип после new — auto x = new Object::Thing;
  • тип в using — using Thing = Object::Thing;
  • заключительный возвращаемый тип auto f() -> Object::Thing;
  • тип по умолчанию в шаблоне template<class T = Object::Thing> T f();
  • тип в static_cast, const_cast, reinterpret_cast, dynamic_cast — auto x = static_cast<Object::Thing>(y);
  • тип переменной/функции в пространстве имён (в том числе в глобальном) или классе — Object::Thing variable;
  • тип параметра функции/шаблона, если есть идентификатор (кроме выражений, связанных с вычислением значения параметра по умолчанию) — void func(Object::Thing x);
template<class T> T::R f();      // Теперь OK, тип в глобальном пространстве имён
template<class T> void f(T::R);  // Нужен typename, без него это попытка создания void-переменной, инициализированной T::R
template<class T> struct S {
  using Ptr = PtrTraits<T>::Ptr; // Теперь OK, тип в using
  T::R f(T::P p) {               // Теперь OK, тип в классе
    return static_cast<T::R>(p); // Теперь OK, static_cast
  }
  auto g() -> S<T*>::Ptr;// Теперь OK, заключительный возвращаемый тип
};
template<typename T> void f() {
  void (*pf)(T::X);   // Остаётся OK, переменная типа void*, инициализированная T::X
  void g(T::X);       // Нужен typename, без него это попытка создания void-переменной, инициализированной T::X
}

Вычисление размера массива в new

Размер массива в операторе new теперь дедуктируется автоматически[29]

double a[]{1,2,3};                // Остаётся OK
double* p = new double[]{1,2,3};  // Теперь OK

Новые атрибуты

  • [[no_unique_address]] — переменная без данных может не занимать места, а в «дырах» переменной с данными можно держать другие переменные. Но: переменные одного типа никогда не могут находиться по одному адресу.
template <class Allocator> class Storage {
private:
  [[no_unique_address]] Allocator alloc;
};
  • [[nodiscard("причина")]] — расширение одноимённого атрибута Си++17. Указывает, что возвращаемое функцией значение нельзя игнорировать, и выводит причину.
class XmlReader {
public:
  [[nodiscard("Проверьте результат или используйте requireTag")]] bool getTag(const char* name);

  void requireTag(const char* name) {
    if (!getTag(name))
      throw std::logic_error(std::string("requireTag: ") + name + " not found");
  }
};
  • [[likely]] / [[unlikely]] — отмечают, под какие ветви надо оптимизировать программу для лучшей работы предсказателя переходов. Эта методика фактически уже реализована в некоторых компиляторах, см. например __builtin_expect в GCC.
if (x > y) [[unlikely]] {
  std::cout << "Редко случается" << std::endl;
} else [[likely]] {
  std::cout << "Часто случается" << std::endl;
}

Расширен constexpr

В constexpr разрешено:

  • вызывать виртуальные функции[30];
  • вызывать деструкторы, которые тоже должны быть constexpr;
  • работать с union[31];
  • работать с try — блок перехвата ничего не делает, а выброс исключения в таком контексте, как и раньше, приводит к ошибке компиляции[32];
  • использовать dynamic_cast и typeid[33];
  • new, с некоторыми ограничениями[34];
  • asm, если тот не вызывается при компиляции;
  • неинициализированные переменные.

Подобная конструкция в теории позволит, например, делать, чтобы константный std::vector просто указывал на память соответствующего std::initializer_list, а обычный неконстантный — отводил динамическую память.

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

Ключевые слова consteval и constinit

constexpr-код не обязан вызываться при компиляции, и достаточно написать std::set<std::string_view> dic { "alpha", "bravo" };, чтобы constexpr-цепочка оборвалась на конструкторе std::set и произошла инициализация при выполнении. Иногда это нежелательно — если переменная используется при инициализации программы (известный недостаток Си++ — неконтролируемый порядок инициализации CPP-файлов), большая (например, большая таблица) или трудновычисляемая (инициализация той же таблицы, проводящаяся за O(n²)). И у программистов бывает просто спортивный интерес перенести код в компиляцию. Чтобы дать уверенность, используются два новых ключевых слова:

  • consteval в функциях: требует, чтобы функция выполнялась при компиляции. Вызов из контекста, невыполнимого при компиляции, запрещён. В заголовках совместимости со старыми компиляторами заменяется на constexpr.
  • constinit в переменной: требует, чтобы переменная вычислилась при компиляции. В заголовках совместимости со старыми компиляторами заменяется на пустую строку.
consteval int sqr(int n)
  { return n * n; }
 
constinit const auto res2 = sqr(5);

int main()
{
  int n;
  std::cin >> n;
  std::cout << sqr(n) << std::endl;   // ошибка, невычислимо при компиляции
}

explicit (bool)

Ключевое слово explicit можно писать вместе с константным булевским выражением: если оно истинно, преобразование возможно только явно. Упрощает метапрограммирование, заменяет идиому SFINAE[35].

// Было, std::forward опущен для краткости
template<class T> struct Wrapper {
  template<class U, std::enable_if_t<std::is_convertible_v<U, T>>* = nullptr>
  Wrapper(U const& u) : t_(u) {}
  
  template<class U, std::enable_if_t<!std::is_convertible_v<U, T>>* = nullptr>
  explicit Wrapper(U const& u) : t_(u) {}

  T t_;
};

// Стало
template<class T> struct Wrapper { 
  template<class U> 
  explicit(!std::is_convertible_v<U, T>) 
    Wrapper(U const& u) : t_(u) {} 

  T t_; 
};

Трёхзначное сравнение («звездолёт»)

Операция <=> позволяет сравнивать объекты по одному из трёх методов:

  • Частичный порядок: меньше, эквивалентны, больше, несравнимы.
  • Слабый порядок: меньше, эквивалентны, больше. Может случиться, что у эквивалентных объектов значение какого-то общедоступного поля или функции может разниться. Понятие «эквивалентны» транзитивно.
  • Сильный (линейный) порядок (меньше, равны, больше). Равные объекты различимы разве что по адресу.
class PersonInFamilyTree { // ...
public:
  std::partial_ordering operator<=>(const PersonInFamilyTree& that) const {
    if (this->is_the_same_person_as ( that)) return partial_ordering::equivalent;
    if (this->is_transitive_child_of( that)) return partial_ordering::less;
    if (that. is_transitive_child_of(*this)) return partial_ordering::greater;
    return partial_ordering::unordered;
  }
};

Название «звездолёт» произошло из старой игры по «Звёздному пути» — этими тремя символами обозначался «Энтерпрайз».

Версия операции «звездолёт» с телом =default просто сравнивает все поля в порядке объявления. Также возможна операция «равняется» с телом =default, она также сравнивает все поля в порядке объявления и автоматически объявляет операцию «не равняется»[36].

Концепции

Концепция — требования к параметрам шаблона, чтобы этот шаблон имел смысл. Большую часть жизни Си++ концепция описывалась устно, со сложными ошибками в априори действующих заголовках вроде STL, если по какой-то причине программист не вписался в концепцию. Если же программист сам пишет шаблон, он может случайно выйти из концепции и не увидеть это на тестовой программе, ведь простейшие типы вроде int имеют множество функций по умолчанию вроде конструктора копирования, присваивания, арифметических операций.

template <class T>
concept bool EqualityComparable() { 
    return requires(T a, T b) {
        {a == b} -> Boolean;  // Концепция, означающая тип, преобразуемый в boolean
        {a != b} -> Boolean;
    };
}

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

Новые условия неявного перемещения

Уточнены условия, когда требуется неявно перемещать объект, особенно при выбросе исключений:[37]

void f() {
  T x;
  try {
    T y;
    try {g(x);}
    catch(...) {
      if(/*...*/)
        throw x;    // не переместит — x снаружи try-блока
      throw y;      // переместит — y внутри try-блока
    }
    g(y);
  } catch(...) {
    g(x);
    // g(y); // ошибка
  }
}

Числа со знаком — дополнительный код

Когда язык Си только зарождался, существовал «зоопарк» разных машин, и учебная машина MIX, придуманная Дональдом Кнутом, отражала это — байт мог хранить от 64 до 100 разных значений, а формат знаковых чисел не оговаривался. За сорок с лишним лет остановились на 8-битном байте и дополнительном коде, и это отметили в стандарте[38].

Арифметическое переполнение в беззнаковой арифметике эквивалентно операциям по модулю, в знаковой — неопределённое поведение.

Библиотека

Мелкие изменения

  • Новые версии make_unique/make_shared, связанные с массивами[39][40].
  • atomic<shared_ptr<>> и atomic<weak_ptr<>>.
  • atomic_ref<>, объект, позволяющий сделать атомарным что угодно[41].
  • std::erase, std::erase_if, упрощают метапрограммирование[42].
  • map.contains[43].
  • Новый заголовок <version> — стандартное место для объявлений, связанных с развитием конкретной стандартной библиотеки[44]. Объявления определяются реализацией.
  • to_address — преобразование указателеподобного объекта в указатель[45]. addressof уже есть, но он требует разыменования, что может стать неопределённым поведением.
  • Новые #define для проверки функциональности компилятора и библиотеки[46]. Стандарты Си++ огромны, и не все разработчики компиляторов быстро вносят их в свои продукты. А некоторые — сбор мусора Си++11 — остаются заглушками и поныне (2021), не реализованные ни в одном компиляторе.
  • Упрощённый карринг через bind_front[47].
  • source_location — обёртка макросов __FILE__ и подобных на Си++.
  • Новый заголовок <numbers> с математическими константами[48]. До этого даже обычные π и e существовали только как расширения.

Объявление функций constexpr

  • std::pointer_traits[49].
  • xxx.empty() и некоторые другие. Запись xxx.empty(); вместо xxx.clear(); стала стандартной ошибкой Си++[50][51], и она объявлена [[nodiscard]].
  • <numeric>[52].
  • конструкторы-деструкторы std::vector и std::string, следствие послаблений в constexpr. На момент проверки (май 2020) ни один компилятор этого не поддерживает[53].

Библиотека форматирования

printf слишком низкоуровневый, опасный и нерасширяемый. Стандартные возможности Си++ позволяют только склеивать строки и потому неудобны для локализации.

Потому в Си++20 сделали более типобезопасный механизм форматирования строк, основанный на Python[54].

char c = 120;
auto s1 = std::format("{:+06d}", c);   // value of s1 is "+00120"
auto s2 = std::format("{:#06x}", 0xa); // value of s2 is "0x000a"
auto s3 = std::format("{:<06}", -42);  // value of s3 is "-42   " (0 is ignored because of < alignment)

Возможности:

  • Один и тот же параметр можно форматировать сколько угодно раз разными способами.
  • Подстановки можно переставлять местами.
  • Выравнивание слева, по центру и справа, любым символом.
  • По умолчанию числа, даты и прочее форматируются локале-нейтрально; если нужна локализация — это задаётся явно.
  • Работает через шаблоны и потому расширяется на любые типы.
  • Скобки можно заэкранировать {{ }}.

Невладеющие указатели на массив (span)

std::string_view оказался отличным объектом, и сделали аналогичное для массивов — std::span[55]. При этом span может изменять содержимое памяти, в отличие от string_view.

void do_something(std::span<int> p) {
    std2::sort(p);
    for (int& v: p) {
        v += p[0];
    }
}
// ...
std::vector<int> v;
do_something(v);

int data[1024];
do_something(data);

boost::container::small_vector<int, 32> sm;
do_something(sm);

Библиотека работы с битами <bit>

Библиотека работы с синхронизированными «потоками вывода» <syncstream>

Поток вывода, как правило, своими силами отрабатывает доступ из разных потоков исполнения. При многопоточном протоколировании возникает задача: собрать данные (например, строку текста) в буфер достаточной длины и одной операцией вывести их в поток.

Для этого используется несложный класс, являющийся потомком ostream.

osyncstream{cout} << "The answer is " << 6*7 << endl;

Весь вывод в подчинённый поток происходит одной операцией в деструкторе.

Библиотека диапазонов <ranges>

Сложная библиотека используется там, где нужно единообразно получить доступ, например, к std::vector и std::deque[56].

Библиотека календарей и часовых поясов в <chrono>

Сложная библиотека для календарных расчётов[57].

auto d1 = 2018_y / mar / 27;
auto d2 = 27_d / mar / 2018;
auto d3 = mar / 27 / 2018;
year_month_day today = floor<days>(system_clock::now());
 
assert(d1 == d2);
assert(d2 == d3);
assert(d3 == today);

Расширенная библиотека потоков <jthread>, <stop_token>

Буква j означает join — то есть при уничтожении объекта-потока система дожидается окончания задачи.

Кроме того, с помощью библиотеки stop_token можно попросить поток остановиться.

#include <thread>
#include <iostream>
 
using namespace std::literals::chrono_literals;
 
void f(std::stop_token stop_token, int value)
{
    while (!stop_token.stop_requested()) {
        std::cout << value++ << ' ' << std::flush;
        std::this_thread::sleep_for(200ms);
    }
    std::cout << std::endl;
}
 
int main()
{
    std::jthread thread(f, 5); // prints 5 6 7 8... for approximately 3 seconds
    std::this_thread::sleep_for(3s);
    // The destructor of jthread calls request_stop() and join().
}

Возможные изменения библиотек

См. также

Примечания

  1. ISO/IEC 14882:2020 (англ.). ISO. Дата обращения: 21 декабря 2020.
  2. Current Status : Standard C++ (англ.).
  3. Herb Sutter. Trip report: Summer ISO C++ standards meeting (Toronto).
  4. P0606R0: Concepts Are Ready (англ.).
  5. Tim Shen, Richard Smith. P0329R4: Designated Initialization Wording (англ.). http://www.open-std.org/.
  6. Thomas Köppe. Allow lambda capture [=, this].
  7. Familiar template syntax for generic lambdas (англ.).
  8. Herb Sutter. Trip report: Fall ISO C++ standards meeting (Albuquerque) (англ.).
  9. Smith, Richard; Perchik, Dawn; Köppe, Thomas N4714 Editors' Report -- Programming Languages -- C++. C++ standards drafts. GitHub. Дата обращения: 27 декабря 2018.
  10. Trip Report: C++ Standards Meeting in Albuquerque, November 2017 (англ.), There's Waldo! (20 November 2017).
  11. Wording for lambdas in unevaluated contexts.
  12. Default constructible and assignable stateless lambdas.
  13. Pack expansion in lambda init-capture. www.open-std.org. Дата обращения: 11 декабря 2017.
  14. String literals as non-type template parameters.
  15. Herb Sutter. Trip report: Summer ISO C++ standards meeting (Rapperswil).
  16. Support for contract based programming in C++. www.open-std.org. Дата обращения: 10 ноября 2018.
  17. C++20 in the fall meeting in November 2018
  18. P1141R1 - Yet another approach for constrained declarations.
  19. P1289R0 - Access control in contract conditions.
  20. P0668R4: Revising the C++ memory model.
  21. P1152R3: Deprecating volatile
  22. Deprecating Vestigial Library Parts in C++17.
  23. Deprecating <codecvt>.
  24. Proposed Resolution for CA 14 (shared_ptr use_count/unique).
  25. P1161R3: Deprecate uses of the comma operator in subscripting expressions (англ.). www.open-std.org. Дата обращения: 21 декабря 2020.
  26. Modules (since C++20) — cppreference.com
  27. Coroutines (C++20) — cppreference.com
  28. Down with typename!
  29. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1009r2.pdf
  30. Allowing Virtual Function Calls in Constant Expressions. www.open-std.org. Дата обращения: 11 марта 2019.
  31. P1330R0 - Changing the active member of a union inside constexpr.
  32. P1002R0 - Try-catch blocks in constexpr functions.
  33. P1327R0 - Allowing dynamic_cast, polymorphic typeid in Constant Expressions.
  34. More constexpr containers (англ.). www.open-std.org. Дата обращения: 21 декабря 2020.
  35. C++20’s Conditionally Explicit Constructors | C++ Team Blog
  36. Default comparisons (since C++20) - cppreference.com
  37. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1825r0.html
  38. P1236R0: Alternative Wording for P0907R4 Signed Integers are Two's Complement.
  39. std::make_unique, std::make_unique_for_overwrite — cppreference.com
  40. std::make_shared, std::make_shared_for_overwrite — cppreference.com
  41. std::atomic_ref — cppreference.com
  42. Adopt Consistent Container Erasure from Library Fundamentals 2 for C++20
  43. std::map<Key,T,Compare,Allocator>::contains — cppreference.com
  44. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0754r2.pdf
  45. Utility to convert a pointer to a raw pointer
  46. Integrating feature-test macros into the C++ WD
  47. Simplified partial function application
  48. Standard library header <numbers> — cppreference.com
  49. P1006R1 - Constexpr in std::pointer_traits.
  50. string::empty — C++ Reference
  51. 100 багов в Open Source проектах на языке Си/Си
  52. Numerics library — cppreference.com
  53. C++20: The Unspoken Features — Human Readable Magazine
  54. Formatting library (C++20) — cppreference.com
  55. Standard library header  — cppreference.com
  56. Ranges library (C++20) — cppreference.com
  57. Extending <chrono> to Calendars and Time Zones
  58. Functional in C++17 and C++20.
  59. P0342R0: Timing barriers.
  60. Task Blocks.
This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.