Avatar ·

Какие новые возможности появились в C++17? Обзор ключевых изменений и примеры

Всем привет! Я изучаю C++ и сейчас пытаюсь разобраться с новыми стандартами. До этого я работал в основном с C++11 и C++14, но тут на проекте коллеги сказали, что пора переходить на C++17. Я открыл документацию, и там столько всего нового, что голова идет кругом. Я слышал про structured bindings, про if constexpr, про std::optional и std::variant. Но как это все работает на практике? Например, я часто пишу код, где нужно вернуть из функции либо значение, либо ошибку. Раньше я использовал кортежи или выходные параметры. Слышал, что в C++17 для этого есть std::optional. Но как его правильно применять? И что такое "гарантированное копирование" (guaranteed copy elision)? Я слышал, что это ускоряет возврат объектов, но не понимаю механизма. Еще меня интересует, как упростилась работа с кортежами и парами. Раньше чтобы разобрать std::pair, нужно было писать std::tie(a, b) = my_pair. А сейчас вроде можно писать auto [a, b] = my_pair. Это правда работает? И самый главный вопрос: что из C++17 реально стоит использовать в продакшене, а что лучше не трогать? Мне нужен не сухой список фич, а практическое руководство: какие нововведения решают реальные проблемы, с примерами кода. Буду благодарен за развернутый ответ с примерами! Вот кусок моего старого кода, который я хочу переписать на C++17:
// Старый способ: возврат пары для результата и флага ошибки
std::pair<int, bool> divide(int a, int b) {
    if (b == 0) return {0, false};
    return {a / b, true};
}

// Использование
auto result = divide(10, 3);
if (result.second) {
    std::cout << result.first;
}
Как это будет выглядеть в C++17? <<>>

1. Структурные привязки (Structured Bindings) — декомпозиция без боли

Да, вы всё правильно поняли. В C++17 появилась возможность "разворачивать" кортежи, пары, массивы и даже структуры в отдельные переменные одной строкой. Ваш пример с делением переписывается так:

#include <iostream>
#include <optional>

std::optional<int> divide(int a, int b) {
    if (b == 0) return std::nullopt;  // нет значения
    return a / b;                     // есть значение
}

int main() {
    auto result = divide(10, 3);
    if (result) {  // проверка на наличие значения
        std::cout << *result;  // разыменование
    }
    
    // А если нужна пара — структурные привязки:
    std::pair<int, std::string> my_pair = {42, "hello"};
    auto [num, str] = my_pair;
    std::cout << num << " " << str;
}

Теперь std::optional<T> — это контейнер, который либо содержит значение типа T, либо пуст. Это гораздо читаемее, чем пара (значение, bool). Метод has_value() или просто operator bool проверяет наличие, а value() или * (оператор разыменования) извлекают значение.

2. if constexpr — условная компиляция на этапе компиляции

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

template <typename T>
auto get_value(T t) {
    if constexpr (std::is_integral_v<T>) {
        return t + 1;
    } else {
        return t + 0.5;
    }
}
// Если T = int, компилятор сгенерирует только первую ветку
// Если T = double — только вторую

3. Гарантированное копирование (Guaranteed Copy Elision)

В C++17 компилятор обязан пропускать копирование при возврате временных объектов. Это значит, что вы можете смело возвращать из функции большие объекты без боязни лишних копий. Раньше это была оптимизация (NRVO), теперь — гарантия:

std::vector<int> create_vector() {
    return std::vector<int>(1000000);  // В C++17 — 0 копий!
}

std::vector<int> v = create_vector(); // прямой конструктор, без копирования

4. std::variant — безопасная замена union

Если вам нужно хранить значение одного из нескольких типов, используйте std::variant:

#include <variant>
std::variant<int, double, std::string> v;
v = 42;                          // хранит int
v = 3.14;                        // теперь хранит double
v = "hello"s;                    // теперь строку

// Посещение (visitor) — как switch по типам:
std::visit([](auto&& arg) {
    std::cout << arg;
}, v);

5. Что реально использовать в продакшене?

  • Обязательно: std::optional, std::variant, std::any (для динамической типизации), структурные привязки, if constexpr, std::string_view (невладеющее представление строки — избегает копирования).
  • Рекомендуется: std::filesystem (работа с файловой системой), std::parallel_algorithms (распараллеливание алгоритмов STL).
  • С осторожностью: std::byte (для низкоуровневой работы), inline переменные (если не уверены в ODR).

Ваш старый код с парой теперь выглядит так:

std::optional<int> divide(int a, int b) {
    if (b == 0) return {};
    return a / b;
}

int main() {
    if (auto result = divide(10, 3); result) {
        std::cout << *result;  // 3
    } else {
        std::cout << "Ошибка";
    }
}

Обратите внимание на if (auto result = ...; result) — это тоже фича C++17: инициализация внутри if.

<<>>

Дополнение: Новые алгоритмы и параллельное выполнение

В C++17 появилась поддержка параллельного выполнения стандартных алгоритмов. Теперь вы можете добавить политику выполнения (execution policy) к любому алгоритму из <algorithm>:

#include <algorithm>
#include <execution>
#include <vector>

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

// Последовательная сортировка (как раньше)
std::sort(std::execution::seq, data.begin(), data.end());

// Параллельная сортировка (использует несколько потоков)
std::sort(std::execution::par, data.begin(), data.end());

// Векторизованная сортировка (может использовать SIMD-инструкции)
std::sort(std::execution::unseq, data.begin(), data.end());

// Параллельная + векторизованная
std::sort(std::execution::par_unseq, data.begin(), data.end());

Это позволяет с минимальными изменениями кода ускорить обработку больших массивов данных. Однако будьте осторожны: параллельные версии требуют, чтобы операции не имели побочных эффектов (не меняли глобальное состояние).

Также стоит упомянуть std::filesystem — полноценную библиотеку для работы с файловой системой:

#include <filesystem>
namespace fs = std::filesystem;

fs::path p = "/home/user/docs";
if (fs::exists(p)) {
    for (auto& entry : fs::directory_iterator(p)) {
        std::cout << entry.path() << '\n';
    }
}

Это намного удобнее, чем использовать POSIX-функции или boost::filesystem.