Перейти к содержимому

C++23 и C++26 Предварительный просмотр

Изучите C++23 std::expected, std::print, вычисляя это, улучшения диапазонов и предпросмотр предстоящих функций C++26, таких как контракты, отражение и сопоставление шаблонов.

Эволюция продолжается

C++23 - это выпуск «завершения», который заполняет пробелы, оставленные C++20, и добавляет высоко запрошенные функции. std::expected обеспечивает обработку ошибок в стиле ржавчины, std::print обеспечивает современный вывод, , что позволяет создавать мощные шаблоны метапрограммирования, а диапазоны получают десятки новых просмотров. Между тем, C++26 становится еще одним крупным релизом с контрактами, отражением и совпадением шаблонов** — функциями, которые кардинально изменят то, как мы пишем безопасный, экспрессивный код C++.

Оригинальное название: Expected: Modern Error Handling

std::expected имеет либо значение успеха типа T, либо ошибку типа E. В сочетании с монадическими операциями (.and_then(), .transform(), .or_else()) он позволяет без исключений обрабатывать ошибки:

expected.cpp
#include <iostream>
#include <expected>
#include <string>
#include <charconv>
#include <system_error>

enum class ParseError {
    empty_input,
    invalid_format,
    out_of_range
};

std::expected<int, ParseError> parse_int(std::string_view sv) {
    if (sv.empty()) return std::unexpected(ParseError::empty_input);

    int result{};
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), result);

    if (ec == std::errc::invalid_argument)
        return std::unexpected(ParseError::invalid_format);
    if (ec == std::errc::result_out_of_range)
        return std::unexpected(ParseError::out_of_range);
    if (ptr != sv.data() + sv.size())
        return std::unexpected(ParseError::invalid_format);

    return result;
}

std::expected<int, ParseError> double_if_positive(int val) {
    if (val <= 0) return std::unexpected(ParseError::out_of_range);
    return val * 2;
}

int main() {
    // Direct usage
    auto result = parse_int("42");
    if (result) {
        std::cout << "Parsed: " << *result << "\n";
    } else {
        std::cout << "Error code: " << static_cast<int>(result.error()) << "\n";
    }

    // Monadic chaining — like Rust's Result::and_then
    auto chained = parse_int("21")
        .and_then(double_if_positive)       // int -> expected<int, E>
        .transform([](int v) { return v + 1; })  // int -> int (wrapped)
        .or_else([](ParseError e) -> std::expected<int, ParseError> {
            std::cout << "Recovering from error\n";
            return 0;  // default value on error
        });

    std::cout << "Chained result: " << *chained << "\n";  // 43

    return 0;
}

std::print и std::println объединяют std::format с выходом, заменяя std::cout << для большинства вариантов использования. Они быстрее, безопасны по типу и производят читаемый код:

print.cpp
#include <print>
#include <vector>
#include <string>
#include <ranges>

int main() {
    // Basic printing — no more << chains!
    std::println("Hello, {}!", "C++23");
    std::print("No newline here. ");
    std::println("But here.");

    // Formatting works just like std::format
    int x = 42;
    double pi = 3.14159;
    std::println("x = {}, pi = {:.2f}", x, pi);
    std::println("Hex: {:#x}, Binary: {:#b}", 255, 255);

    // Print to stderr
    std::println(stderr, "Warning: something happened");

    // Containers and ranges (C++23 formatter support)
    std::vector<int> v{1, 2, 3, 4, 5};
    std::println("Vector: {}", v);  // [1, 2, 3, 4, 5]

    // Formatted table
    std::println("{:<15} {:>10} {:>10}", "Name", "Score", "Grade");
    std::println("{:<15} {:>10} {:>10}", "Alice", 95, "A");
    std::println("{:<15} {:>10} {:>10}", "Bob", 87, "B+");

    return 0;
}

Улучшения диапазонов: zip, chunk, slide

C++23 добавляет мощные новые виды диапазона, которые охватывают общие шаблоны, ранее требующие ручных циклов или сторонних библиотек:

ranges_cpp23.cpp
#include <print>
#include <ranges>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> names{"Alice", "Bob", "Carol"};
    std::vector<int> scores{95, 87, 92};

    // zip — combine multiple ranges element-wise
    for (auto [name, score] : std::views::zip(names, scores)) {
        std::println("{}: {}", name, score);
    }

    std::vector<int> data{1, 2, 3, 4, 5, 6, 7, 8, 9};

    // chunk — split into groups of N
    for (auto chunk : data | std::views::chunk(3)) {
        std::print("[ ");
        for (int v : chunk) std::print("{} ", v);
        std::println("]");
    }
    // [ 1 2 3 ] [ 4 5 6 ] [ 7 8 9 ]

    // slide — sliding window of size N
    for (auto window : data | std::views::slide(3)) {
        std::print("( ");
        for (int v : window) std::print("{} ", v);
        std::println(")");
    }
    // ( 1 2 3 ) ( 2 3 4 ) ( 3 4 5 ) ... ( 7 8 9 )

    // cartesian_product — all combinations
    std::vector<char> suits{'H', 'D', 'C', 'S'};
    std::vector<int> ranks{1, 2, 3};
    for (auto [suit, rank] : std::views::cartesian_product(suits, ranks)) {
        std::print("{}{}  ", suit, rank);
    }
    std::println("");

    // enumerate — index + value (finally!)
    for (auto [i, name] : std::views::enumerate(names)) {
        std::println("[{}] {}", i, name);
    }

    return 0;
}

Выводить это

C++23, выводящий этот* (также называемый «явным параметром объекта»), позволяет функциям-членам принимать this в качестве явного, выведенного параметра. Это устраняет необходимость дублирования перегрузок const/non-const, обеспечивает рекурсивные лямбда и упрощает CRTP:

struct Widget {
    std::string name;

    // Before C++23: two identical overloads
    // const std::string& get_name() const { return name; }
    // std::string& get_name() { return name; }

    // C++23: one function handles both
    template<typename Self>
    auto&& get_name(this Self&& self) {
        return std::forward<Self>(self).name;
    }
};

Параметр this Self&& self определяет, является ли объект const, non-const, lvalue или rvalue — и пересылает правильно. Этот паттерн, называемый «выводящим эту» идиому, может сократить наборы перегрузок функции члена пополам.

C++26 Превью: Контракты, Отражение, Соответствие образов

Ожидается, что C++26 будет включать в себя несколько преобразующих функций:

Контракты — pre, post и contract_assert допускают предварительные условия, пост-условия и утверждения о том, что система сборки может включать или отключать:

int sqrt_int(int x)
    pre(x >= 0)
    post(r: r * r <= x)
{
    // implementation
}

Static Reflection — Проверка типов, членов и переписчиков во время составления. Генерировать код на основе структурных полей, автопроизводной сериализации и многого другого — все без макросов.

Соответствие шаблонов — выражения inspect позволяют сопоставлять типы, значения и структуру:

inspect (variant_value) {
    <int> i    => std::println("int: {}", i);
    <string> s => std::println("string: {}", s);
    __         => std::println("other");
};

Отправитель/получатель (std::execution) Стандартная структура для выполнения асинхронизации, которая обеспечивает структурированную параллель, заменяя специальные реализации пула потоков.

Лучшая практика

Оставайтесь в курсе эволюции C++:

1. Задайте минимальный стандарт для вашего проекта (C++17 является прочной основой для большинства производственных кодов сегодня)
2. Используйте флаги компилятора для выбора стандарта: -std=c++17, -std=c++20, -std=c++23
3. Проверить поддержку компилятора на cppreference.com/compiler support — не все компиляторы реализуют каждую функцию
4. Использовать тестовые макросы , такие как __cpp_concepts, __cpp_lib_expected для условного использования новых функций
5. Читайте предложения (wg21.link/pXXXX) для функций, которые вы планируете использовать
6. Не гонитесь за каждой новой функцией — примите то, что делает вашу кодовую базу проще и правильнее, а не просто новее

Подводный камень

Принятие новейших стандартных функций слишком рано имеет реальные затраты:

Ошибки компилятора: Новые функции часто имеют ошибки реализации, которые обнаруживаются только при использовании продукта. Например, модули C++20 имели значительные ошибки компилятора в течение 2-3 лет после стандартизации.

Неполный инструментарий: Отладчики, профилировщики и статические анализаторы могут не полностью поддерживать новейшие функции. Поддержка IDE (IntelliSense, завершение кода) часто отстает.

**Портативность: Если ваш код должен компилироваться на нескольких платформах (Linux, macOS, Windows, встроенные), вы ограничены пересечением того, что поддерживают все целевые компиляторы.

Готовность команды: Новые функции требуют общекомандного обучения. Функция, которую понимает только один разработчик, создает узкое место в обслуживании.

Ключевые выводы
  • std::expected позволяет обрабатывать ошибки в стиле Rust с помощью монадных цепей (.and_then(), .transform())
  • std::print / std::println заменит std::cout << с чистым, быстрым, безопасным по типу форматированию выходом
  • Диапазоны C++23 включают zip, chunk, slide, cartesian_product и enumerate
  • Уменьшение этого устраняет дублирование функции Const/non-const
  • C++26 нацелен на контракты, статическое отражение, сопоставление шаблонов и std::execution для асинхронизации
  • Принятие новых стандартов постепенно — проверка поддержки компилятора, тщательное тестирование и обеспечение готовности команды.

Тест — проверьте свои знания

(15 XP)

1. Что представляет собой `std::expected<int, Error>`?

2. Что производит `std::views::zip(names, scores)`?

3. Какая функция C++26 позволяет определять предусловия функции и постусловия?