Дамы и господа, здравствуйте.

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

Комментарии (36)


  1. NelSon29
    26.12.2017 10:49
    +2

    Спасибо за работу и перевод современных книг, очень радует, что у нас этим занимаются.
    Сама же книга, увы, допускает ряд неточностей, некачественных и неэффективных примеров.


    Чего только стоит

    Повсеместное использование

    using namespace std;


    1. Jamdaze
      26.12.2017 13:59

      Ну, для примеров в книге это простительно, код становится компактнее и читабельнее.


      1. NelSon29
        26.12.2017 14:36

        Сейчас примеры почистили, но до сих пор остались сомнительные моменты, о которых более подробно расписали в комментариях на реддите.


        1. Yarique
          26.12.2017 21:33

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


          using namespace std;


  1. maaGames
    26.12.2017 11:00

    Вообще, очень сладенькие нововведения, хотя мне потребуется время, чтобы глаз перестал видеть ошибки в новом синтаксисе там, где их нет. И начал видеть так, где они есть.
    Например, я до сих пор в шоке, что вместо emplace_back(make_pair(10,«a»)); можно написать emplace_back(10,«a»);
    Код гораздо чище становится, конечно, но надо перепривыкать… И как бы всё это в итоге не вытекло в отладочный Адъ.


    1. broken
      27.12.2017 08:01

      Это же std::forward в конструктор пары, еще с 11 стандарта так можно писать.


    1. Yarique
      28.12.2017 13:38

      шаблоны с переменным числом аргументов c++ погуглите ну и std::forward, всё логично


      1. maaGames
        28.12.2017 13:41

        Счастливые вы люди… Я лишь недавно смог перейти на 2008 студию.(


        1. Yarique
          28.12.2017 14:06

          Разрабатываете под винду? Сочувствую


          1. maaGames
            28.12.2017 14:07

            Хуже! Под винду в 2008 студии. Сочувствие в квадрате.


  1. Jamdaze
    26.12.2017 14:29

    Почему типам variant, optional, any уделяется столько внемания? Все кто хотел их уже давно используют из буста.


    1. RPG18
      26.12.2017 15:21

      Тем, что теперь не надо будет тянуть буст.


    1. Yarique
      28.12.2017 13:39

      std:: variant, optional, any лучше boost:: variant, optional, any


  1. pproger
    26.12.2017 14:35

    книгу скачал, спасибо, на праздниках почитаю.


  1. Jeka178RUS
    26.12.2017 14:35
    +1

    Активное использование auto, вот главная проблема. Особенно больно видеть в объявлении функции. Элементарный пример: функция возвращает кортеж каких-то типов, используется привязка, все переменные объявлены как auto, что есть что непонятно, смотрим объявление функции, опять auto!!! Придется смотреть тело функции что бы понять, а что же на самом деле возвращает эта функция. Это мрак.


    1. AndreySu
      26.12.2017 19:26

      Можно не писать везде auto, но там где возвращаются ацкие шаблонные шаблоны то выглядит не плохо.


      1. Jeka178RUS
        26.12.2017 19:34

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

        auto [name, age] = get();
        std::cout << name << " is " << age << std::endl;
        


    1. Dima_Sharihin
      27.12.2017 08:32

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


      Все эти std::get сами по себе костыль, ни слова не говорящие о содержимом кортежа.


  1. Cheater
    26.12.2017 14:45

    Filesystem Technical Spec же забыли!

    ИМХО гораздо более приятно видеть простые утилитарные нововведения (пресловутый filesystem API или давно ожидаемые модули) чем синтаксические новинки (constexpr lambda итд), которые применимы только если вся команда гарантированно хорошо разбирается в этом.


    1. Jeka178RUS
      26.12.2017 19:29

      Согласен, меня тоже больше интересует расширение стандартной библиотеки, нежели новые трюки.


    1. FoxCanFly
      28.12.2017 20:56

      constexpr lambda не синтаксическая новинка, а просто исправление предыдущих стандартов с целью консистентности подхода (потому что незвозможнсть лямбды быть constexpr нелогична). Это как раз очень полезно, так как на уровне библиотек не эмулируется, чего не скажешь о filesystem. Все его всю жизнь из boost использовали и не страдали от этого


  1. Yarique
    26.12.2017 21:45

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

    Достаточно инстанцировать для структуры/класса
    std::get
    std::tuple_element
    std::tuple_size
    и можно


    auto [n, a, [c1, c2]] = p2; 

    писать


    1. Yarique
      27.12.2017 16:15

      auto& [n, a, Loc] = p2; 
      auto& [c1, c2] = Loc;

      так можно для любой структуры


  1. AxisPod
    27.12.2017 08:47

    Объем опечаток, явных ошибок и отсутствие части материала соответствует станадрту «Packt>»?

    Есть 4 книги в одной не хватает пары страниц, в других ошибки с опечатками исчислюются очень большими числами, как-то боязно пользоваться книгами данного издательства.


    1. Yarique
      27.12.2017 13:26

      вот тоже последнее время решил брать оригиналы на английском


  1. aamonster
    27.12.2017 12:21

    1. s/необходимость в std::get отпадает/необходимость в std::tie отпадает/
    2. "Зачем использовать std::variant" — вовсе не потому, что union чем-то плох (он решает свои задачи), а потому, что рано или поздно оказывается нужен именно variant — с явным хранением информации о типе. И каждый пишет свой велосипед. Лучше бы пример нормальный к нему привели — не try-catch, а хоть тот же visitor. И сказали бы, проконтроллирует ли компилятор обработку всех вариантов. И насколько хорошо он оптимизируется.
    3. Про std::optional — тот же вопрос про оптимизацию. Например, если мы заменяем nullable pointer на std::optional — будет ли оверхед строго нулевым (с выигрышем по надёжности и простоте кода за счёт value_or и т.п.)

    Конечно, ответ на все эти вопросы нетрудно найти и самому, но необходимости прямо сейчас — нету (лично мне не перейти так вдруг на 2017)


    Ну и банальное: variant/optional/any давно были в boost, кто хотел — тот пользовался. Так что тут скорее уместен вопль 'ура-ура, они есть у нас 'из коробки"'.


    1. Yarique
      27.12.2017 13:17

      boost::variant может алоцировать память в куче, std::variant не аллоцирует память в куче никогда, так что это не тупо копия, а меньший оверхед


    1. Yarique
      27.12.2017 13:24

      std::optional и std::any тоже имеют меньший оверхэд, не принижайте людей из комитета стандартизации, там не дураки сидят


      1. aamonster
        27.12.2017 14:11

        Отлично (хотя и странно, в boost тоже умные люди пишут — но, возможно, какие-то оптимизации были недоступны).
        Раз уж вы не поленились разобраться, то скажите: std::optional уже оптимизирован до предела? Т.е. можно всюду, где мы возвращали (возможно нулевой) указатель, возвращать optional и не проиграть ни в памяти, ни в быстродействии? Или пока нет?


        1. Yarique
          27.12.2017 14:50

          Человек, который boost::variant писал(Антон Полухин, в Москве(иногда и в Сибири) его можно найти на C++ User Group Meetup), признаётся, что std::variant имеет меньший оверхэд


          boost всегда реализуют без учёта "математики компиляторов".
          В Core WG и в Library WG комитета стандартизации С++ все правки рассматривают с точки зрения всех популярных компиляторов, воспринимая компиляторы как математические объекты, всё что в std:: добавляется из boost:: всегда будет, благодаря Core WG и Library WG, иметь меньший оверхэд.


        1. Yarique
          27.12.2017 14:52

          std::optional надо будет раскурить подробнее, пока точно не могу сказать
          Антон, думаю тоже может лучше меня объяснить, в любом случае


    1. Yarique
      27.12.2017 13:25

      std::tie всё также нужен для лексиграфического сравнения


      std::tie(a,b) < std::tie(rhs.a, rhs.b);


    1. FoxCanFly
      28.12.2017 20:58

      std::tie нужен не только для распаковки tuple. Кортеж ссылок — нередкий кейс.


      1. aamonster
        29.12.2017 00:01

        Насчёт tie — я всего лишь поправлял описку в статье.


  1. Yarique
    27.12.2017 13:18

    std::tie всё также нужен для лексиграфического сравнения


    std::tie(a,b) < std::tie(rhs.a, rhs.b);


  1. marataziat
    28.12.2017 13:50

    Как скоро поддержка нового c++ будет на codeforces и подобных сайтах?