Мы как раз закончили перевод интересной книги Яцека Галовица о 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 и отличную статью Бартека.

