Сусакент
- 1 year ago
- 0
- 0
C++20 — название стандарта ISO /IEC языка программирования C++ . Спецификация опубликована в декабре 2020 года .
Комитет по стандартам C++ начал планировать C++20 в июле 2017 года . C++20 является преемником C++17 .
Константа
__cplusplus
увеличилась до
202002L
.
Так как модификатор
volatile
является машинозависимым и семантика операций над ним и количество обращений к памяти не ясны, для межпоточной синхронизации лучше использовать
atomic
.
Запрещены следующие операции с
volatile
-переменными
:
++
,
--
;
+=
и другие (снят в
C++23
);
volatile
;
volatile
, кроме некоторых вроде
remove_volatile
;
Для
atomic
добавлены дополнительные функции, компенсирующие то, что запретили.
В предыдущих стандартах агрегатная инициализация разрешалась, если конструктор был помечен как
default
или
delete
, что вводило пользователей в заблуждение: объект инициализируется в обход конструктора.
struct X {
int a = 0;
X() = default;
};
X x { 5 }; // Си++17: OK
// Си++20: no matching constructor for initialization of 'X'
Удалены редкие возможности стандартной библиотеки, запрещённые в C++17:
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 — из-за неочевидного поведения и желания создать новый синтаксис для многомерных массивов
. Если очень нужно, пишите
a
[(
b
,
c
)]
.
<=>
, трёхзначное сравнение).
<=>
, трёхзначное сравнение). Хотя бы один надо преобразовать в указатель.
is_pod
— вместо сложного понятия «
простая структура данных
» лучше использовать конкретные свойства типа: тривиально строится, тривиально уничтожается и т. д. Если очень надо (например, для передачи данных между
плагинами
), эквивалентно
is_trivial
&&
is_standard_layout
.
std
::
rel_ops
— новая операция «звездолёт» делает это лучше.
shared_ptr
— непонятно, как работать с указателем, атомарно или нет. Лучше это определить системой типов,
atomic
<
shared_ptr
>
.
string
::
capacity
()
— теперь решили, что
reserve
не будет уменьшать ёмкость.
string
::
reserve
()
— добавили запрещённую версию без параметров, эквивалентную
shrink_to_fit
. (До Си++11, да и после тоже, функция
reserve
(
0
)
была эквивалентна
shrink_to_fit
.)
filesystem
::
u8path
— теперь
u8string
отличается от
string
.
ATOMIC_FLAG_INIT
,
atomic_init
,
ATOMIC_VAR_INIT
— теперь это делает шаблонный конструктор
atomic
.
using
EnumClass
, позволяющий сделать код в ключевых местах менее загромождённым.
for
(
T
thing
=
f
();
auto
&
x
:
thing
.
items
())
. Если возвращаемый
items
()
объект
временный
, его срок жизни расширяется на весь цикл, но другие временные объекты благополучно исчезают, и если временный на поверку f(), запись
for
(
auto
&
x
:
f
().
items
())
ошибочная.
Директива компилятора
#include
в своё время была удобным механизмом Си, который, был, по сути, кроссплатформенным ассемблером, «паразитировавшим» на ассемблерных утилитах —
линкере
и библиотекаре. Отсюда важная черта компиляторов Си — они первыми после ассемблера появлялись на новых платформах. Но с расширением проектов квадратично повышалось время их компиляции: увеличивалось как количество
единиц трансляции
, так и количество подключённых к ним заголовков. Механизм модулей был долгим объектом споров ещё со времён Си++11.
В Си++20 он вошёл в таком виде :
// helloworld.cpp
export module helloworld; // module declaration
import <iostream>; // import declaration
export void hello() { // export declaration
std::cout << "Hello world!\n";
}
Сопрограмма
— это специальная бесстековая функция, которая может приостановить своё исполнение, пока выполняется другая функция
. Состояние сопрограммы хранится в динамической памяти (кроме случаев, когда оптимизатору удалось избавиться от выделения). Выглядит как обычная функция, но содержит особые сопрограммные ключевые слова
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
>
обеспечивает
ленивое вычисление
.
По факту на Си++23 стандартная библиотека сопрограмм не выработана, и слово за экспериментаторами.
В некоторых местах шаблонов слово
typename
(объяснение, что
Object
::
Thing
— это тип, а не функция) больше не требуется
. К таким местам относятся…
new
—
auto
x
=
new
Object
::
Thing
;
using
—
using
Thing
=
Object
::
Thing
;
auto
f
()
->
Object
::
Thing
;
template
<
class
T
=
Object
::
Thing
>
T
f
();
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 теперь дедуктируется автоматически
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 { // считыватель XML потокового типа
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
;
union
;
try
— блок перехвата ничего не делает, а выброс исключения в таком контексте, как и раньше, вычислит функцию при исполнении
;
dynamic_cast
и
typeid
;
new
, с некоторыми ограничениями
;
asm
, если тот не вызывается при компиляции;
Подобная конструкция в теории позволит, например, делать, чтобы константный std::vector просто указывал на память соответствующего , а обычный неконстантный — отводил динамическую память.
Расширен вызов лямбда-функций при компиляции — например, можно отсортировать .
constexpr-код не обязан вызываться при компиляции, и достаточно написать
std
::
set
<
std
::
string_view
>
dic
{
"alpha"
,
"bravo"
};
, чтобы constexpr-цепочка оборвалась на конструкторе
и произошла инициализация при выполнении. Иногда это нежелательно — если переменная используется при инициализации программы (известный недостаток Си++ — неконтролируемый порядок инициализации 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
можно писать вместе с константным булевским выражением: если оно истинно, преобразование возможно только явно. Упрощает метапрограммирование, заменяет идиому
SFINAE
.
// Было, 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_;
};
[[
nodiscard
]]
для типов и конструкторов
Для типа: запрещается любое неиспользование временного объекта данного типа.
Для конструктора: запрещается вызов конструктора и потом неиспользование его результата. .
struct [[nodiscard]] my_scopeguard { /* ... */ };
struct my_unique {
my_unique() = default; // не захватывает ресурса
[[nodiscard]] my_unique(int fd) {} // захватывает ресурс
~my_unique() noexcept {}
};
struct [[nodiscard]] error_info {};
error_info enable_missile_safety_mode();
void launch_missiles();
void test_missiles() {
my_scopeguard(); // Предупреждение
void(my_scopeguard()), // OK
launch_missiles(); // OK: операция «запятая», оператор продолжается
my_unique(42); // Предупреждение
my_unique(); // OK
enable_missile_safety_mode(); // Остаётся предупреждение
launch_missiles();
}
error_info &foo();
void f() { foo(); } // OK: ссылка никогда не nodiscard
Операция
<=>
позволяет сравнивать объекты по одному из трёх методов:
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
, она также сравнивает все поля в порядке объявления и автоматически объявляет операцию «не равняется»
.
Концепция — требования к параметрам шаблона, чтобы этот шаблон имел смысл. Большую часть жизни Си++ концепция описывалась устно, со сложными ошибками в заведомо действующих заголовках вроде STL, если программист не вписался в концепцию. Если же программист сам пишет шаблон, он может случайно выйти из концепции и не увидеть это на тестовой программе, ведь простейшие типы вроде
int
имеют множество функций по умолчанию вроде конструктора копирования, присваивания, арифметических операций.
template <class T>
concept bool EqualityComparable() {
return requires(T a, T b) {
{a == b} -> Boolean; // Концепция, означающая тип, преобразуемый в boolean
{a != b} -> Boolean;
};
}
Обработка строк при компиляции была давней мечтой Си++, и очередной шажок к ней — строковые константы в шаблонах . В частности, хотелось бы преобразовывать регулярные выражения в байт-код уже при компиляции. На экспериментальных библиотеках регулярных выражений уже видели ускорение до 3000 раз по сравнению с .
template <auto& str>
void f() {
// str = char const (&)[7]
}
f<"foobar">();
Порядковая инициализация структур Си
Point
p
{
10
,
20
};
ошибкоопасна, если ожидается расширение структуры или два соседних элемента можно спутать. В новый стандарт добавилось
Point
p
{
.
x
=
10
,
.
y
=
20
};
, давно существовавшее в Си, но не формализированное в Си++
.
Кроме того, такая конструкция позволяет инициализировать именно тот вариант
union
, который нужно.
union FloatInt {
float asFloat;
int32_t asInt;
};
FloatInt x { .asInt = 42 };
Удалены по сравнению с Си:
int
arr
[
3
]
=
{[
1
]
=
5
};
— начиная с Си++11 квадратные скобки в начале выражения означают лямбда-функцию.
Point
p
{
.
y
=
20
,
.
x
=
10
};
— конфликтует с автодеструкторами Си++: сконструировали в одном порядке, разрушили в другом?
struct
B
b
=
{.
a
.
x
=
0
};
— редко используются
Point
p
{.
x
=
1
,
2
};
Лямбда-функции появились в Си++11 вдогонку за другими языками программирования. Решают сразу несколько вопросов: заменяют препроцессор, если надо исполнить один и тот же код в двух местах функции, а в отдельный объект/функцию вынести трудоёмко; переносят текст функции ближе к тому месту, где он требуется; позволяют писать в функциональном стиле. Названы так в честь лямбда-исчисления , одной из основ функционального программирования.
Явный перехват объекта
в лямбда-функции
[=, this](){}
и
[=, *this](){}
. Как сказано выше, неявный перехват
this
в лямбда-функциях запретили.
Традиционный синтаксис лямбда-шаблонов
вместо Си++14
[](
auto
x
)
. Этот синтаксис удобнее, если нужно сделать самопроверку, или вычислить какой-нибудь производный тип
.
// Было
auto f = [](auto vector) {
using T = typename decltype(vector)::value_type;
...
};
// Стало
auto f = []<typename T>(std::vector<T> vector) {
...
};
Лямбда-функции в невычисляемых контекстах : сигнатурах, возвращаемых типах, параметрах шаблонов .
std::priority_queue<
int, // тип элемента
std::vector<int>, // тип контейнера
decltype( [](int a, int b)->bool{ // тип функции сравнения элементов
return a>b;
})> q;
Чтобы этот код работал, нужно ещё одно изменение — лямбда-функция без перехватов теперь имеет конструктор по умолчанию и операцию присваивания . Все экземпляры этого псевдокласса выполняют одно и то же, и никак нельзя заставить данную очередь с приоритетами сравнивать в другом порядке. Конструкторы копирования и перемещения были изначально у всех лямбда-функций.
В списке перехвата лямбда-функции теперь можно держать операцию развёртывания вариативной части — раньше для этого приходилось подключать объект-кортеж. Например, данный шаблон возвращает лямбда-функцию, которую при желании можно вызвать когда угодно — она вызывает функцию foo() и уже содержит копии всех нужных для вызова данных.
// Было
template <class... Args>
auto delay_invoke_foo(Args... args) {
return [tup=std::make_tuple(std::move(args)...)]() -> decltype(auto) {
return std::apply([](auto const&... args) -> decltype(auto) {
return foo(args...);
}, tup);
};
}
// Стало
template <class... Args>
auto delay_invoke_foo(Args... args) {
return [args=std::move(args)...]() -> decltype(auto) {
return foo(args...);
};
}
Уточнены условия, когда требуется неявно перемещать объект, особенно при выбросе исключений:
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-битном байте и дополнительном коде , в первую очередь из-за простоты и интероперабельности , и это отметили в стандарте .
Арифметическое переполнение в беззнаковой арифметике эквивалентно операциям по модулю , в знаковой — неопределённое поведение .
Устно нерекомендуемый с Си++17
memory_order_consume
, предназначенный для PowerPC и ARM, формализован и возвращается в обиход. Усилен
memory_order_seq_cst
.
make_unique
/
make_shared
, связанные с массивами
.
atomic
<
shared_ptr
<>>
и
atomic
<
weak_ptr
<>>
.
atomic_ref
<>
, объект, позволяющий сделать атомарным что угодно
.
std
::
erase
,
std
::
erase_if
, упрощают метапрограммирование
.
map
.
contains
.
<
version
>
— стандартное место для объявлений, связанных с развитием конкретной стандартной библиотеки
. Объявления определяются реализацией.
to_address
— преобразование указателеподобного объекта в указатель
.
addressof
уже есть, но он требует разыменования, что может стать
неопределённым поведением
.
#define
для проверки функциональности компилятора и библиотеки
. Стандарты Си++ огромны, и не все разработчики компиляторов быстро вносят их в свои продукты. А некоторые —
сбор мусора
Си++11 — остаются заглушками и поныне (2021), не реализованные ни в одном компиляторе.
bind_front
.
source_location
— обёртка макросов
__FILE__
и подобных на Си++.
<
numbers
>
с математическими константами
. До этого даже обычные
π
и
e
существовали только как расширения.
string
.
reserve
(
n
)
и другие больше не уменьшают ёмкость
.
cmp_equal
и другие, безопасно сравнивающие числа — расширением до общего типа или ещё каким-то образом
. Используется в первую очередь в обобщённом программировании.
std
::
pointer_traits
.
xxx
.
empty
()
и некоторые другие. Запись
xxx
.
empty
();
вместо
xxx
.
clear
();
стала стандартной ошибкой Си++
, и она объявлена
[[
nodiscard
]]
.
<
numeric
>
.
atomic
,
atomic_flag
.
printf слишком низкоуровневый, опасный и нерасширяемый. Стандартные возможности Си++ позволяют только склеивать строки и потому неудобны для локализации .
Потому в Си++20 сделали более типобезопасный механизм форматирования строк, основанный на Python .
char c = 120;
auto s1 = std::format("{:+06d}", c); // "+00120"
auto s2 = std::format("{:#06x}", 0xa); // "0x000a"
auto s3 = std::format("{:<06}", -42); // "-42 " (0 игнорируется из-за выравнивания <)
Возможности:
{{ }}
.
оказался отличным объектом, и сделали аналогичное для массивов — . При этом 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);
Поток вывода , связанный с объектом ОС (файлом или устройством), как правило, своими силами отрабатывает доступ из разных потоков исполнения . При многопоточном протоколировании возникает задача: собрать данные (например, строку текста) в буфер достаточной длины и одной операцией вывести их в поток.
Для этого используется несложный класс, являющийся потомком
ostream
.
osyncstream{cout} << "The answer is " << 6*7 << endl;
Весь вывод в подчинённый поток происходит одной операцией в деструкторе.
Сложная библиотека используется там, где нужно единообразно получить доступ, например, к std::vector и .
Сложная библиотека для календарных расчётов .
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);
Буква 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().
}
Барьер ( barrier ) — механизм межпоточной блокирующей синхронизации, действующий так: как только у барьера соберутся n потоков, он исполнит объект-функцию и отпустит их. Обычно используется для периодической координации частично распараллеливаемых задач: после того, как потоки исполнят каждый свою долю, срабатывает координатор и решает, что делать дальше.
Засов ( latch ) — облегчённый одноразовый барьер .
Основное назначение: ключи хранения — «тяжёлые» объекты (например,
string
), но в качестве ключа поиска допустимы и облегчённые:
и даже const char*. Реализовано оно крайне просто: добавлена шаблонная функция find, принимающая любой тип, сам же разнородный поиск включается типом-маркером
is_transparent
. Поддерживаются четыре функции: find, count, equal_range, contains. В Си++23 ожидается больше функций, поддерживающих разнородный поиск — например, erase
.
Для самобалансирующихся деревьев поиска ( / ) реализовано в Си++14.
Эта функция не включена по умолчанию из-за ошибкоопасности: преобразование типов может не сохранять те соотношения, на которых работает контейнер. Например,
1.0
<
1.1
, но
static_cast
<
int
>
(
1.0
)
==
static_cast
<
int
>
(
1.1
)
. Потому поиск дробного числа в
set
<
int
>
приведёт не к тому, что надо
. Так что программист сам должен допустить те альтернативные ключи, которые заведомо годятся.
struct string_hash {
using is_transparent = void;
[[nodiscard]] size_t operator()(const char *txt) const {
return std::hash<std::string_view>{}(txt);
}
[[nodiscard]] size_t operator()(std::string_view txt) const {
return std::hash<std::string_view>{}(txt);
}
[[nodiscard]] size_t operator()(const std::string &txt) const {
return std::hash<std::string>{}(txt);
}
};
std::unordered_map<std::string, int, string_hash, std::equal_to<>> m {
{ "Hello Super Long String", 1 },
{ "Another Longish String", 2 },
{"This cannot fall into SSO buffer", 3 }
};
bool found = m.contains("Hello Super Long String");
std::cout << "Found: " << std::boolalpha << found << '\n';