Это шестая и последняя часть. В нем рассказывается о других новшествах ядра и стандартной библиотеки, добавленных стандартом C++20.

  1. Модули и краткая история C++.
  2. Операция «Космический корабль.
  3. понятия.
  4. диапазоны.
  5. Корутины.
  6. Другие функции ядра и стандартной библиотеки. Заключение.

Другие основные функции

Я рассказал о самых значимых новшествах Стандарта, но это лишь капля в море 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 😊

лямбды

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

  1. квадратные скобки для связываемых переменных,
  2. угловые скобки для параметров шаблона,
  3. скобки для списка аргументов,
  4. фигурные скобки для тела функции.

Порядок важен: если их перепутать, будет ошибка. Если параметров шаблона нет, то угловые скобки не пишите, так как они не могут быть пустыми. Кстати, эта возможность уже нормально поддерживается во всех компиляторах.

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 constant
consteval 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_t
    std::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++ уже есть некоторые предложения о том, как это могло бы выглядеть.

Ждать и смотреть.