Самые полезные новинки C++ 20
В сентябре прошлого года профильный комитет ISO утвердил С++ 20 в качестве текущей версии международного стандарта. Предлагаю ознакомиться с самыми полезными и долгожданными изменениями нового стандарта.
Библиотека концепций C++
Библиотека определяет фундаментальные понятия, которые могут быть использованы для диспетчеризации функций и проверки аргументов шаблона во время компиляции, на основе свойств типов. Концепции нужны для того, чтобы можно было избежать логических противоречий между свойствами типов данных внутри шаблона и таковыми входных параметров. Концепция должна определяться в пределах пространства имен и имеет следующий вид.
template <список параметров>
concept concept-name = constraint-expression;
...
// concept
template
concept Derived = std::is_base_of::value;
Каждая концепция является предикатом, который оценивается при компиляции и становится частью интерфейса шаблона, где используется в качестве ограничения:
#include
#include
#include
template
concept Sorter = requires(T a) {
{ std::hash{}(a) } -> std::convertible_to;
};
struct asdf {};
template
void f(T) {}
int main() {
using std::operator«»s;
f(«abc»s); // Верно, std::string удовлетворяет условиям Sorter
//f(asdf{}); // Ошибка: asdf не удовлетворяет условиям Sorter
}
Вслед за директивами #include следует объявление концепции Sorter, которой удовлетворяет любой тип T такой, что для значений a типа T компилируется выражение std: hash{}(a), а его результат преобразуется в std: size_t. Если в main вызвать f (asdf), то получим вполне осмысленную ошибку компиляции.
main.cpp: In function 'int main()':
main.cpp:18:9: error: use of function 'void f(T) [with T = asdf]' with unsatisfied constraints
18 | f(asdf{}); // Ошибка: asdf не удовлетворяет условиям Sorter
| ^
main.cpp:13:6: note: declared here
13 | void f(T) {}
| ^
main.cpp:13:6: note: constraints not satisfied
main.cpp: In instantiation of 'void f(T) [with T = asdf]':
main.cpp:18:9: required from here
main.cpp:6:9: required for the satisfaction of 'Sorter' [with T = asdf]
main.cpp:6:18: in requirements with 'T a' [with _Tp = asdf; T = asdf]
main.cpp:7:21: note: the required expression 'std::hash<_Tp>{}(a)' is invalid
7 | { std::hash{}(a) } -> std::convertible_to
| ~~~~~~~~~~~~~~^~~
cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail
Еще компилятор преобразует концепцию, как и requires-expression в значение типа bool и затем они могут использоваться как простое значение, например, в if constexpr.
template
concept Meshable = requires(T a, T b)
{
a + b;
};
template
void f(T x)
{
if constexpr(Meshable){ /*...*/ }
else if constexpr(requires(T a, T b) { a + b; }){ /*...*/ }
}
Requires-expression
Новое ключевое слово в C++20 существует в двух значениях: requires clause и requires-expression. Несмотря на значительную полезную нагрузку, эта двойственность requires приводит к путанице.
В requires-expression используется тип bool, код в фигурных скобках вычисляется при компиляции. Если выражение корректно requires-expression возвращает true, иначе — false. Первая странность заключается в том, что код в фигурных скобках должен быть написан на специально придуманном языке, не на C++.
template
constexpr bool Movable = requires(T i) { i>>1; };
bool b1 = Movable; // true
bool b2 = Movable; // false
Главный сценарий использования requires-expression состоит в создании концепций, просто проверить наличие нужных полей и методов внутри типа.
template
concept Vehicle =
requires(T v) { // любая переменная m из концепции Vehicle
v.start(); // обязательно должна обладать `v.start()`
v.stop(); // и `v.stop()`
};
Однако, у requires-expression есть и другие применения. Часто необходимо проверить, обеспечивает ли данный набор параметров шаблона требуемый интерфейс: свободные функции, функции-члены, связанные типы и т. д.
template
void smart_swap(T& a, T& b)
{
constexpr bool have_element_swap = requires(T a, T b){
a.swap(b);
};
if constexpr (have_element_swap) {
a.swap(b);
}
else {
using std::swap;
swap(a, b);
}
}
Requires clause
Чтобы действительно что-то ограничить, нам нужен requires clause. Его можно применять к любой шаблонной декларации, или не-шаблонной функции, чтобы выявить является ли та видимой в определенном контексте. Основная польза от requires clause в том, его использование позволяет забыть о SFINAE и прочих странных обходных решениях шаблонов C++.
template
void f(T&&) requires Eq;
template requires Dividable
T divide(T a, T b) { return a/b; }
В декларации requires clause возможно использование нескольких предикатов, объединенных логическими операторами && или ||.
template
requires is_standard_layout_v && is_trivial_v
void fun(T v);
int main()
{
std::string s;
fun(1); // верно
fun(s); // ошибка компиляции
}
Из-за двойственной сути ключевого слова requires могут возникать ситуации с эталонным неудобочитаемым кодом.
template
requires Sumable
auto f1(T a, T b) requires Subtractable; // Sumable && Subtractable
auto l = [] requires Sumable
(T a, T b) requires Subtractable{};
template
requires Sumable
class C;
template
requires requires(T a, T b) {a + b;}
auto f4(T x);
То самое requires requires, первое знамением clause, второе же — expression.
Модули
В C++ проглядывается долгосрочная тенденция, которая выражена в постепенном исключении препроцессора. Считается, что это избавит от целого ряда трудностей:
- заголовки, зависящие от порядка включения;
- утечка макросов из заголовочных файлов;
- повторная компиляция одного и того же кода;
- циклические зависимости;
- плохая инкапсуляция деталей реализации.
Так например source_location заменяет один из наиболее часто используемых макросов, а consteval — макрофункции. Новый способ разделения исходного кода использует модули и призван полностью заменить все директивы #include.
Вот как выглядит модульный Hello World!…
//module.cpp
export module speech;
export const char* get_phrase() {
return «Hello, world!»;
}
//main.cpp
import speech;
import ;
int main() {
std::cout << get_phrase() << '\n';
}
Сопрограммы
Сопрограммой называется функция, которая может остановить выполнение, чтобы быть возобновлённой позже. Такая функция не имеет стека, она приостанавливает выполнение, возвращаясь к вызывающей инструкции. C++ 20 предоставляет практически самый низкоуровневый API, оставляя все прочее на усмотрение пользователя.
Функция является сопрограммой, если в её определении используется одно из следующих действий.
- оператор co_await для приостановки выполнения до возобновления;
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));
}
}
- ключевое слова co_yield для приостановки выполнения, возвращающего значение;
generator iota(int n = 0) {
while(true)
co_yield n++;
}
- ключевое слова co_return для завершения выполнения, возвращающего значение.
lazy f() {
co_return 7;
}
Сопрограммы не могут использовать простые операторы return, типы auto, или Concept и переменные аргументы.
Оператор KK
В C++ 20 появился оператор трехстороннего сравнения <=> и сразу получил прозвище spaceship operator, что означает оператор космический корабль. Данный оператор для двух переменных a и b определяет одно из трех: a > b, a=b или a < b. Оператор <=> можно задать самостоятельно, или компилятор автоматически создаст его для вас.
Проще всего понять на примере для чего именно нужен новый оператор трехстороннего сравнения.
#include
struct Data
{
int i;
int j;
bool operator<(const Data& rhs) const {
return i < rhs.i || (i == rhs.i && j < rhs.j);
}
};
int main()
{
std::set d;
d.insert(Data{ 1,2 });
}
Возникает такое впечатление, что многовато кода bool operator<… для простого оператора ради того, чтобы не возникло ошибок компиляции. Ну, а если нужны и другие операторы: >, ==, ≤, ≥ неудобно каждый раз выводить весь этот блок. Теперь же благодаря оператору <=> то же самое мы получаем более простым способом.
Обратите внимание, что нам понадобился дополнительный заголовочный файл, поэтому #include. На самом деле мы получили больше, чем запрашивали, так как теперь мы можем использовать разом все операторы сравнения, а не только <.
#include
#include
struct Data
{
int i;
int j;
auto operator<=>(const Data& rhs) const = default;
};
int main()
{
Data d1{ 1, 4 };
Data d2{ 3, 2 };
d1 == d2;
d1 < d2;
d1 <= d2;
std::set d;
d.insert(Data{ 1,2 });
}
Наши серверы можно использовать для тестирования и продакшена на плюсах.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!