Самые полезные новинки C++ 20



    В сентябре прошлого года профильный комитет ISO утвердил С++ 20 в качестве текущей версии международного стандарта. Предлагаю ознакомиться с самыми полезными и долгожданными изменениями нового стандарта.

    Библиотека концепций C++


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

    template <список параметров>
    
    concept concept-name = constraint-expression;
    
    ...
    
    template <class T, class U>
    
    concept Derived = std::is_base_of<U, T>::value;

    Каждая концепция является предикатом, который оценивается при компиляции и становится частью интерфейса шаблона, где используется в качестве ограничения:

    #include <string>
    
    #include <cstddef>
    
    #include <concepts>
    
    template<typename T>
    
    concept Sorter = requires(T a) {
    
        { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
    
    };
    
    struct asdf {};
    
    template<Sorter T>
    
    void f(T) {}
    
    int main() {
    
      using std::operators;
    
      f(«abc»s); <i>// Верно, std::string удовлетворяет условиям Sorter</i>
    
      <i>//f(asdf{}); // Ошибка: asdf не удовлетворяет условиям Sorter</i>
    
    }

    Вслед за директивами #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{}); <i>// Ошибка: asdf не удовлетворяет условиям Sorter</i>
    
          |         ^
    
    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<T>' [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<T>{}(a) } -> std::convertible_to<std::size_t>
    
          |       ~~~~~~~~~~~~~~^~~
    
    cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail

    Еще компилятор преобразует концепцию, как и requires-expression в значение типа bool и затем они могут использоваться как простое значение, например, в if constexpr.

    template<typename T>
    
    concept Meshable = requires(T a, T b)
    
    {
    
        a + b;
    
    };
    
    template<typename T>
    
    void f(T x)
    
    {
    
        if constexpr(Meshable<T>){ <i>/*...*/</i> }
    
        else if constexpr(requires(T a, T b) { a + b; }){ <i>/*...*/</i> }
    
    }
    

    Requires-expression


    Новое ключевое слово в C++20 существует в двух значениях: requires clause и requires-expression. Несмотря на значительную полезную нагрузку, эта двойственность requires приводит к путанице.

    В requires-expression используется тип bool, код в фигурных скобках вычисляется при компиляции. Если выражение корректно requires-expression возвращает true, иначе — false. Первая странность заключается в том, что код в фигурных скобках должен быть написан на специально придуманном языке, не на C++.

    template<typename T>
    
    constexpr bool Movable = requires(T i) { i>>1; };
    
    bool b1 = Movable<int>; <i>// true</i>
    
    bool b2 = Movable<double>; <i>// false</i>
    
    Главный сценарий использования <i>requires-expression</i> состоит в создании концепций, просто проверить наличие нужных полей и методов внутри типа.
    
    template <typename T>
    
    concept Vehicle =
    
      requires(T v) {  <i>// любая переменная m из концепции Vehicle</i>
    
        v.start();     <i>// обязательно должна обладать `v.start()`</i>
    
        v.stop();      <i>// и `v.stop()`</i>
    
      };

    Однако, у requires-expression есть и другие применения. Часто необходимо проверить, обеспечивает ли данный набор параметров шаблона требуемый интерфейс: свободные функции, функции-члены, связанные типы и т. д.

    template <typename T>
    
    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<typename T>
    
    void f(T&&) requires Eq<T>;
    
    template<typename T> requires Dividable<T>
    
    T divide(T a, T b) { return a/b; }

    В декларации requires clause возможно использование нескольких предикатов, объединенных логическими операторами && или ||.

    template <typename T>
    
      requires is_standard_layout_v<T> && is_trivial_v<T>
    
    void fun(T v);
    
    int main()
    
    {
    
      std::string s;
    
      fun(1);  <i>// верно</i>
    
      fun(s);  <i>// ошибка компиляции</i>
    
    }

    Из-за двойственной сути ключевого слова requires могут возникать ситуации с эталонным неудобочитаемым кодом.

    template<typename T>
    
    requires Sumable<T>
    
    auto f1(T a, T b) requires Subtractable<T>; <i>// Sumable<T> && Subtractable<T></i>
    
    auto l = []<typename T> requires Sumable<T>
    
        (T a, T b) requires Subtractable<T>{};
    
    template<typename T>
    
    requires Sumable<T>
    
    class C;
    
    template<typename T>
    
    requires requires(T a, T b) {a + b;}
    
    auto f4(T x);
    

    То самое requires requires, первое знамением clause, второе же — expression.

    Модули


    В C++ проглядывается долгосрочная тенденция, которая выражена в постепенном исключении препроцессора. Считается, что это избавит от целого ряда трудностей:

    • заголовки, зависящие от порядка включения;
    • утечка макросов из заголовочных файлов;
    • повторная компиляция одного и того же кода;
    • циклические зависимости;
    • плохая инкапсуляция деталей реализации.

    Так например source_location заменяет один из наиболее часто используемых макросов, а consteval — макрофункции. Новый способ разделения исходного кода использует модули и призван полностью заменить все директивы #include.

    Вот как выглядит модульный Hello World!..

    <i>//module.cpp</i>
    
    export module speech;
    
    export const char* get_phrase() {
    
        return «Hello, world!»;
    
    }
    
    <i>//main.cpp</i>
    
    import speech;
    
    import <iostream>;
    
    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<int> iota(int n = 0) {
    
      while(true)
    
        co_yield n++;
    
    }


    • ключевое слова co_return для завершения выполнения, возвращающего значение.

    lazy<int> f() {
    
      co_return 7;
    
    }


    Сопрограммы не могут использовать простые операторы return, типы auto, или Concept и переменные аргументы.

    Оператор KK


    В C++ 20 появился оператор трехстороннего сравнения <=> и сразу получил прозвище spaceship operator, что означает оператор космический корабль. Данный оператор для двух переменных a и b определяет одно из трех: a > b, a=b или a < b. Оператор <=> можно задать самостоятельно, или компилятор автоматически создаст его для вас.

    Проще всего понять на примере для чего именно нужен новый оператор трехстороннего сравнения.

    #include <set>
    
    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<Data> d;
    
    	d.insert(Data{ 1,2 });
    
    }


    Возникает такое впечатление, что многовато кода bool operator<… для простого оператора ради того, чтобы не возникло ошибок компиляции. Ну, а если нужны и другие операторы: >, ==, ≤, ≥ неудобно каждый раз выводить весь этот блок. Теперь же благодаря оператору <=> то же самое мы получаем более простым способом.

    Обратите внимание, что нам понадобился дополнительный заголовочный файл, поэтому #include . На самом деле мы получили больше, чем запрашивали, так как теперь мы можем использовать разом все операторы сравнения, а не только <.

    #include <set>
    
    #include <compare>
    
    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<Data> d;
    
    	d.insert(Data{ 1,2 });
    
    }




    Наши серверы можно использовать для тестирования и продакшена на плюсах.

    Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

    Маклауд
    Облачные серверы на базе AMD EPYC

    Комментарии 2

      +12

      Я надеюсь, что это всего лишь плохой перевод плохой статьи. Ну пожалуйста. Нельзя же так.

        +1

        Символы — это какие-то ошмётки от неправильного форматирования?

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое