[Перевод] Новинки С++17, которые необходимо использовать каждому
Дамы и господа, здравствуйте.
Мы как раз закончили перевод интересной книги Яцека Галовица о 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
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 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 age;
age = 51;
auto a = std::get(age);
Если попытаться использовать член, который не задан таким образом, то программа выбросит исключение:
try {
std::cout << std::get(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
using namespace std::experimental;
optional 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
using namespace std::experimental;
std::vector 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(v[1]) << std::endl;
Можно воспользоваться членом type()
, чтобы получить объект type_info
, сообщающий, что содержится в any
. Требуется точное соответствие между типами, в противном случае программа выбросит исключение std::bad_any_cast
:
try {
std::cout << any_cast(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 и отличную статью Бартека.