C++17: структурированные привязки, контейнеры и новые типы
C++17 – релиз, который расширил возможности C++: в языке появились структурированные привязки, контейнеры и новые типы данных.
Контейнеры и новые типы
В версии 17 в стандартной библиотеке C++ появилось много полезных типов данных. Простейший – std::byte. Это просто единичный байт, который предназначен для работы с хранилищем данных.
std::variant
Этот тип умеет в заданное время хранить в себе значение какого-то альтернативного типа. Представьте, что поле с количеством полных лет пользователя будет реализовано как строковая дата и целочисленное значение:
std::variant<uint32_t, std::string> age; age = 51; auto a = std::get<uint32_t>(age);
std::variant взаимодействует с объектами, которые не относятся к POD-типам. В этом его отличие и преимущество перед обычными объединениями.
std::optional
std::optional является объектом, который может как хранить значение, так и быть пустым. Он будет полезен в ситуации, когда функция не возвращает значение – тогда им может стать std::optional. Использование std::optional также снижает вероятность случайного использования пустого значения.
#include <experimental/optional> using namespace std::experimental; optional<int> convert(const std::string& s) { try { int res = std::stoi(s); return res; } catch(std::exception&) { return {}; } } int v = convert("123").value_or(0); std::cout << v << std::endl; int v1 = convert("abc").value_or(0); std::cout << v1 << std::endl;
Здесь определяется функция, которая должна преобразовать строку в целое число. С использованием std::optional при неудаче вернется ноль.
std::any
Предоставляет типобезопасный контейнер для единственного значения любого типа. Можно проверить содержимое std::any и достать из него значение, использовав std::any_cast:
#include <experimental/any> using namespace std::experimental; std::vector<any> v { 1, 2.2, false, "hi!" }; auto& t = v[1].type(); // Что содержится в этом std::any? if (t == typeid(double)) std::cout << "We have a double" << "\n"; else std::cout << "We have a problem!" << "\n"; std::cout << any_cast<double>(v[1]) << std::endl;
Также можно применить type() при проверке содержимого, чтобы не получить исключение.
try { std::cout << any_cast<int>(v[1]) << std::endl; } catch(std::bad_any_cast&) { std::cout << "wrong type" << std::endl; }
std::any поможет там, где помогал указатель void*, только будет типобезопасным. Он позволит представить значение «5» и в виде целого, и в виде строки.
Структурированные привязки и множественное присваивание
Структурированные привязки – полезная новинка C++17. Они позволяют делать множественные привязки к структурированным типам, вроде массивов и кортежей. Это нужно, чтобы, например, присвоить все члены структуры независимым переменным единственным вызовом. Использование структурных привязок делает код в целом более чистым и понятным.
Кортежи
Эта структура появилась в C++11. Они аналогичны массивам и представляют собой коллекции с фиксированной длиной. Кортежи можно использовать, чтобы вернуть несколько значений функции, например так:
#include <tuple> auto get() { return std::make_tuple("fred", 42); }
Этот пример вернет кортеж с двумя членами. В C++14 версии появилась возможность использовать auto с возвращаемыми типами функций, чтобы сделать вызов функции более аккуратным. Для получения значений кортежа, при этом, будет требоваться std::get:
auto t = get(); std::cout << std::get<0>(t) << std::endl;
Через std::tie можно привязать элементы кортежа к переменным, которые уже должны быть объявлены:
std::string name; int age; std::tie(name, age) = get();
Благодаря этому методу можно привязывать элементы кортежа к переменным без std::get и необходимости объявлять переменные заранее:
auto [name, age] = get(); std::cout << name << " is " << age << std::endl;
Этот метод также позволяет получить ссылки к элементам кортежа, что невозможно с std::tie:
auto t2 = std::make_tuple(10, 20); auto& [first, second] = t2; first += 1; std::cout << "value is now " << std::get<0>(t2) << std::endl;
Массивы и структуры
Кроме кортежей, структурированные привязки можно применять к массивам и структурам:
struct Person { std::string name; uint32_t age; std::string city; }; Person p1{"bill", 60, "New York"}; auto [name, age, city] = p1; std::cout << name << "(" << age << ") lives in " << city << std::endl;
То же с массивами:
std::array<int32_t, 6> arr{10, 11, 12, 13, 14, 15}; auto [i, j, k, l, _dummy1, _dummy2] = arr;
Но в этих примерах есть недостатки:
- Структурированные привязки не применяются к части элементов, только ко всем сразу. Это обусловлено ограничениями std::tie. Если какие-то элементы не нужно привязывать, можно использовать подготовленные для этого мусорные переменные, как в примере с массивом (_dummy1, _dummy2).
- Деструктуризация будет работать только на один уровень вглубь.
К примеру, в Person имеется элемент Location:
struct Location { std::string city; std::string country; }; struct Person { std::string name; uint32_t age; Location loc; };
Соберем Person и Location, используя вложенную инициализацию:
Person2 p2{"mike", 50, {"Newcastle", "UK"}};
В заключение, все это работает только для классов с публичными и нестатичными данными. Основные компиляторы, вроде GCC, Clang и MSVC уже поддерживают нововведения. Подробности описаны на cppreference.