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.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Senior Java Developer
Москва, по итогам собеседования
Java Team Lead
Москва, по итогам собеседования

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