Avatar ·

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

Всем привет! Я пишу на C++ уже около года, в основном использую C++11/14. Недавно начал изучать C++17 и заметил, что много кода в open-source проектах использует конструкции, которых я не понимаю. Например, я часто вижу такой код:
auto [x, y, z] = getPoint();
Или такое:
if (auto it = map.find(key); it != map.end()) { ... }
Или вот ещё:
std::variant<int, float, std::string> v = 42;
Мой вопрос: какие именно нововведения принёс C++17? Я слышал про structured bindings, if-init, std::variant, std::optional, но как всё это работает на практике? Есть ли что-то ещё, что стоит знать? Меня особенно интересуют: 1. Какие фичи реально упрощают повседневную разработку? 2. Что изменилось в стандартной библиотеке? 3. Есть ли какие-то подводные камни при переходе с C++14 на C++17? Буду благодарен за подробный разбор с примерами кода. Хочу понять, какие фичи стоит внедрять в свои проекты, а от каких пока воздержаться. Спасибо! <<>>

Основные нововведения C++17 с примерами

C++17 — это эволюционный шаг вперёд, который добавил множество удобных возможностей. Разберём их по категориям.

1. Структурные привязки (Structured Bindings)

Позволяют распаковывать кортежи, пары и структуры в отдельные переменные одной строкой:

// До C++17
std::pair<int, double> p = getValues();
int a = p.first;
double b = p.second;

// C++17
auto [a, b] = getValues(); // a - int, b - double

// Работает и с массивами
int arr[3] = {1, 2, 3};
auto [x, y, z] = arr; // x=1, y=2, z=3

Важно: структурные привязки создают копии элементов. Для ссылок используйте auto&.

2. Инициализация в условии (if/switch with initializer)

Позволяет объявлять переменную прямо в условии, ограничивая её область видимости:

// Вместо:
{
    auto it = myMap.find(key);
    if (it != myMap.end()) {
        process(it->second);
    }
}

// Можно так:
if (auto it = myMap.find(key); it != myMap.end()) {
    process(it->second);
} // it выходит из области видимости здесь

// Аналогично для switch:
switch (char c = getChar(); c) {
    case 'a': handleA(); break;
    case 'b': handleB(); break;
}

3. std::variant — типобезопасная "сумма типов"

Замена объединениям и сырым union:

#include <variant>
#include <iostream>

std::variant<int, float, std::string> v;

v = 42;                    // хранит int
v = 3.14f;                // хранит float
v = "hello"s;             // хранит string

// Посещение с помощью std::visit
std::visit([](auto&& arg) {
    std::cout << arg << '\n';
}, v);

// Проверка типа
if (std::holds_alternative<int>(v)) {
    std::cout << "int: " << std::get<int>(v);
}

4. std::optional — значение может отсутствовать

Безопасная замена "магическим" значениям (например, -1 или nullptr):

std::optional<int> findValue(const std::vector<int>& v, int target) {
    auto it = std::find(v.begin(), v.end(), target);
    if (it != v.end()) return *it;
    return std::nullopt;
}

// Использование
auto result = findValue(data, 42);
if (result) {
    std::cout << "Found: " << *result;
} else {
    std::cout << "Not found";
}

// Или с value_or
int val = result.value_or(-1);

5. std::string_view — лёгкое представление строк

Не владеющий памятью просмотр строки — идеально для параметров функций:

void processName(std::string_view name) {
    // name не копирует строку
    for (char c : name) { ... }
}

// Можно передавать std::string, const char*, литералы
processName("John");       // без создания std::string
std::string s = "Doe";
processName(s);            // тоже без копирования

6. Вывод шаблонных аргументов для классов (CTAD)

Компилятор может вывести типы для шаблонов классов:

// До C++17
std::pair<int, double> p1(1, 2.5);
std::vector<int> v1 = {1, 2, 3};

// C++17
std::pair p2(1, 2.5);       // auto
std::vector v2 = {1, 2, 3}; // auto
std::mutex mtx;
std::lock_guard lck(mtx);   // auto

7. Новые алгоритмы и параллельные версии

C++17 добавил параллельные версии алгоритмов:

#include <execution>
#include <algorithm>

std::vector<int> data = ...;

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

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

// Новые алгоритмы: gcd, lcm, clamp
int g = std::gcd(12, 18);    // 6
int c = std::clamp(x, 0, 100); // ограничить x в [0,100]

8. Вложенные пространства имён

// Вместо:
namespace A { namespace B { namespace C { ... } } }

// Можно:
namespace A::B::C { ... }

Подводные камни и рекомендации

  • CTAD не работает с агрегатной инициализацией: std::pair p{1, 2.5}; — ошибка, нужно std::pair p(1, 2.5);
  • std::string_view не гарантирует нуль-терминацию — будьте осторожны при передаче в C-функции.
  • std::variant может бросать исключение std::bad_variant_access при неправильном доступе.
  • Параллельные алгоритмы требуют поддержки от компилятора и библиотеки (TBB, Intel Parallel STL).

В целом, C++17 — очень удачный стандарт. Начните с std::optional, std::string_view и структурных привязок — они сразу упростят код.

<<>>

Дополнительные фичи и практические советы

Хочу добавить несколько менее известных, но полезных возможностей C++17, которые часто упускают в обзорах.

1. constexpr if (if constexpr)

Позволяет выполнять условную компиляцию внутри шаблонов:

template<typename T>
auto getValue(T t) {
    if constexpr (std::is_integral_v<T>) {
        return t + 1;  // для int
    } else {
        return t;      // для float/string
    }
}

Это заменяет SFINAE и специализации во многих случаях.

2. Fold expressions (свёртки) для variadic templates

template<typename... Args>
auto sum(Args... args) {
    return (... + args); // бинарная левая свёртка
}

// sum(1, 2, 3) → ((1 + 2) + 3) = 6

3. Упрощённое лямбда-выражение (constexpr лямбды)

Лямбды теперь могут быть constexpr и захватывать *this по значению:

auto lambda = [](auto x) constexpr { return x * 2; };
constexpr int result = lambda(5); // работает в compile-time

4. std::filesystem

Кроссплатформенная работа с файловой системой:

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

fs::path p = "dir/file.txt";
if (fs::exists(p)) {
    auto size = fs::file_size(p);
    auto time = fs::last_write_time(p);
}

Совет по миграции

Начните с включения флага -std=c++17