Pull to refresh

Новинки С++17, которые необходимо использовать каждому

Издательский дом «Питер» corporate blog Programming *C++ *ООP *
Translation
Original author: Julian Templeman
Дамы и господа, здравствуйте.

Мы как раз закончили перевод интересной книги Яцека Галовица о STL С++ 17, которую надеемся выпустить чем раньше, тем лучше.


Сегодня же мы хотим предложить вашему вниманию перевод статьи Джулиана Темплмана с сайта «O'Reilly» с небольшим анонсом возможностей стандартной библиотеки нового стандарта С++.

Всех — с наступающим новым годом!

C++17 – крупный новый релиз, в нем более 100 новых возможностей и существенных изменений. Если говорить о крупных изменениях, то в новой версии не появилось ничего сравнимого по значимости со ссылками rvalue, которые мы получили в C++11, однако, есть масса изменений и дополнений, например, структурированные привязки и новые контейнерные типы. Более того, проделана большая работа, чтобы весь язык С++ стал более согласованным, разработчики постарались убрать из него бесполезные и ненужные поведения – например, поддержку триграфов и std::auto_ptr.

В этой статье мы обсудим два важнейших нововведения C++17, без которых разработчику совершенно не обойтись при создании современного кода на C++. Речь пойдет о структурированных привязках, обеспечивающих удобный новый способ работы со структурированными типами, а также о некоторых новых типах и контейнерах, которые добавились в Стандартную Библиотеку.

Структурированные привязки для множественного присваивания


Структурированные привязки – совершенно новый феномен, и при этом очень полезный. Они обеспечивают множественное присваивание от структурированных типов (например, кортежей, массивов и структур) – например, присваивание всех членов структуры отдельным переменным в единственной инструкции присваивания. Так код получается компактнее и понятнее.
Примеры кода со структурными привязками запускают на Linux при помощи коммпилятора clang++ версии 4 с флагом -std=c++1z, активирующим возможности 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();

Однако, работая со структурированными привязками в C++17, можно связывать члены кортежей непосредственно с именованными переменными, и тогда необходимость в 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;

Вывод покажет, что значение t2 изменилось с 10 на 11.

Структурированные привязки для массивов и структур


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

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 — приходится привязывать все элементы. Поэтому невозможно, к примеру, извлечь из массива лишь первые четыре элемента. Если вы хотите частично извлечь cтруктуру или массив, то просто подставьте переменные-заглушки для тех членов, что вам не нужны, как показано в примере с массивом.
Во-вторых (и это разочарует программистов, привыкших использовать такую идею в функциональных языках, например, в Scala и Clojure), деструктуризация действует лишь на один уровень в глубину. Допустим, у меня в структуре 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"}};

Можно предположить, что привязка в данном случае пригодится и для доступа к членам, но на практике оказывается, что такая операция недопустима:

auto [n, a, [c1, c2]] = p2;  // не скомпилируется

Наконец, отмечу, что извлекать таким образом члены можно лишь из тех классов, в которых нужные вам данные являются общедоступными и нестатическими. Подробнее этот вопрос рассмотрен в следующей статье о структурированных привязках.

Новые библиотечные типы и контейнеры


В Стандартную Библиотеку в C++17 также добавилось множество новых и полезных типов данных, причем, некоторые из них зародились в Boost.
Код из этого раздела был протестирован в Visual Studio 2017.

Вероятно, самый простой тип std::byte – он представляет отдельный байт. Для представления байт разработчики традиционно пользовались char (знаковым или беззнаковым), но теперь есть тип, который может быть не только символом или целым числом; правда, байт можно преобразовывать в целое число и обратно. Тип std::byte предназначен для взаимодействия с хранилищем данных и не поддерживает арифметических операций, хотя, поддерживает побитовые операции.

std::variant

Концепция “вариант” может показаться знакомой тем, кто имел дело с Visual Basic. Вариант – это типобезопасное объединение, которое в заданный момент времени содержит значение одного из альтернативных типов (причем, здесь не может быть ссылок, массивов или 'void').

Простой пример: допустим, есть некоторые данные, где возраст человека может быть представлен в виде целого числа или в виде строки с датой рождения. Можно представить такую информацию при помощи варианта, содержащего беззнаковое целое число или строку. Присваивая целое число переменной, мы задаем значение, а затем можем извлечь его при помощи std::get, вот так:

std::variant<uint32_t, std::string> age;
age = 51;
 
auto a = std::get<uint32_t>(age);

Если попытаться использовать член, который не задан таким образом, то программа выбросит исключение:

try {
 	std::cout << std::get<std::string>(age) << std::endl;
}
catch (std::bad_variant_access &ex) {
 	std::cout << "Doesn't contain a string" << std::endl;
}

Зачем использовать std::variant, а не обычное объединение? В основном потому, что объединения присутствуют в языке прежде всего ради совместимости с C и не работают с объектами, не относящимися к POD-типам. Отсюда, в частности, следует, что в объединение не так-то просто поместить члены с копиями пользовательских конструкторов копирования и деструкторов. С std::variant таких ограничений нет.

std::optional

Другой тип, std::optional, удивительно полезен и на практике предоставляет возможности, существующие во многих функциональных языках. 'optional' – это объект, который может содержать либо не содержать значения; этот объект удобно использовать в качестве возвращаемого значения функции, когда она не может вернуть значение; тогда он служит альтернативой, например, нулевому указателю.

Работая с optional, мы приобретаем дополнительное преимущество: теперь возможность отказа функции явно обозначена прямо в объявлении, и, поскольку приходится извлекать значение из optional, значительно снижается вероятность, что мы случайно используем нулевое значение.

В следующем примере определяется функция преобразования, пытающаяся превратить строку в целое число. Возвращая optional, функция оставляет такую возможность: может быть передана недопустимая строка, преобразовать которую не удастся. Вызывающая сторона использует функцию value_or, чтобы получить значение из 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::any

Наконец, есть std::any, предоставляющий типобезопасный контейнер для одиночного значения любого типа (при условии, что оно обладает конструктором при копировании). Можно проверить, содержит ли 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(), чтобы получить объект type_info, сообщающий, что содержится в any. Требуется точное соответствие между типами, в противном случае программа выбросит исключение std::bad_any_cast:

try {
  std::cout << any_cast<int>(v[1]) << std::endl;
} catch(std::bad_any_cast&) {
  std::cout << "wrong type" << std::endl;
}

Когда может пригодиться такой тип данных? Простой ответ – во всех случаях, когда можно было бы воспользоваться указателем void*, но в данном случае гарантируется типобезопасность. Например, вам могут понадобиться разные представления базового значения: допустим, представить '5' и в виде целого числа, и в виде строки. Подобные случаи распространены в интерпретируемых языках, но могут пригодиться и в случаях, когда требуется представление, которое не будет автоматически преобразовываться.

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

Важнейшие компиляторы, в том числе, GCC, Clang и MSVC, уже поддерживают многие из этих нововведений; подробнее об этом рассказано здесь.

В интернете есть несколько очень неплохих резюмирующих статей с описанием различных нововведений, появившихся в С++17, среди которых я бы особо отметил статью Тони ван Эрда, подробную статью на StackOverflow и отличную статью Бартека.
Tags:
Hubs:
Total votes 23: ↑22 and ↓1 +21
Views 32K
Comments Comments 38

Information

Founded
Location
Россия
Website
piter.com
Employees
201–500 employees
Registered