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.

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
29 марта 2020

ТОП-10 книг по C++: от новичка до профессионала

Книги по C++ на русском языке с лучшими оценками. Расставлены в порядке воз...
Библиотека программиста
31 января 2019

Лучшие инструменты и советы начинающему C++ программисту

Хотите изучать C++? Делимся важными навыками, фреймворками и советами, кото...