Недавно в финском городе Оулу завершилась встреча международной рабочей группы WG21 по стандартизации C++, в которой впервые официально участвовали сотрудники Яндекса. На ней утвердили черновой вариант C++17 со множеством новых классов, методов и полезных нововведений языка.
Во время поездки мы обедали с Бьярне Страуструпом, катались в лифте с Гербом Саттером, жали руку Беману Дейвсу, выходили «подышать воздухом» с Винцентом Боте, обсуждали онлайн-игры с Гором Нишановым, были на приёме в мэрии Оулу и общались с мэром. А ещё мы вместе со всеми с 8:30 до 17:30 работали над новым стандартом C++, зачастую собираясь в 20:00 чтобы ещё четыре часика поработать и успеть добавить пару хороших вещей.
Теперь мы готовы поделиться с вами «вкусностями» нового стандарта. Всех желающих поглядеть на многопоточные алгоритмы, новые контейнеры, необычные возможности старых контейнеров, «синтаксический сахар» нового чудесного C++, прошу под кат.
if constexpr (condition)
В C++17 появилась возможность на этапе компиляции выполнять if:
template <std::size_t I, class F, class S>
auto rget(const std::pair<F, S>& p) {
if constexpr (I == 0) {
return p.second;
} else {
return p.first;
}
}
При этом неактиваная ветка ветвления не влияет на определение возвращаемого значения. Другими словами, данный пример скомпилируется и:
- при вызове rget<0>( std::pair<char*, short>{} ) тип возвращаемого значения будет short;
- при вызове rget<1>( std::pair<char*, short>{} ) тип возвращаемого значения будет char*.
T& container::emplace_back(Args&&...)
Методы emplace_back(Args&&...) для sequence контейнеров теперь возвращают ссылку на созданый элемент:
// C++11
some_vector.emplace_back();
some_vector.back().do_something();
// C++17
some_vector.emplace_back().do_something();
std::variant<T...>
Позвольте представить: std::variant<T...> — union, который помнит что хранит.
std::variant<int, std::string> v;
v = "Hello word";
assert(std::get<std::string>(v) == "Hello word");
v = 17 * 42;
assert(std::get<0>(v) == 17 * 42);
Дизайн основан на boost::variant, но при этом убраны все известные недочёты последнего:
- std::variant никогда не аллоцирует память для собственных нужд;
- множество методов std::variant являются constexpr, так что его можно использовать в constexpr выражениях;
- std::variant умеет делать emplace;
- к хранимому значению можно обращаться по индексу или по типу — кому как больше нравится;
- std::variant не нуждается в boost::static_visitor;
- std::variant не умеет рекурсивно держать в себе себя (например функционал наподобие `boost::make_recursive_variant<int, std::vector< boost::recursive_variant_ >>::type` убран).
Многопоточные алгоритмы
Практически все алгоритмы из заголовочного файла были продублированы в виде версий, принимающих ExecutionPolicy. Теперь, например, можно выполнять алгоритмы многопоточно:
std::vector<int> v;
v.reserve(100500 * 1024);
some_function_that_fills_vector(v);
// Многопоточная сортировка данных
std::sort(std::execution::par, v.begin(), v.end());
Осторожно: если внутри алгоритма, принимающего ExecutionPolicy, вы кидаете исключение и не ловите его, то программа завершится с вызовом std::terminate():
std::sort(std::execution::par, v.begin(), v.end(), [](auto left, auto right) {
if (left==right)
throw std::logic_error("Equal values are not expected"); // вызовет std::terminate()
return left < right;
});
Доступ к нодам контейнера
Давайте напишем многопоточную очередь с приоритетом. Класс очереди должен уметь потокобезопасно сохранять в себе множество значений с помощью метода push и потокобезопасно выдавать значения в определенном порядке с помощью метода pop():
|
|
В C++17 многие контейнеры обзавелись возможностью передавать свои внутренние структуры для хранения данных наружу, обмениваться ими друг с другом без дополнительных копирований и аллокаций. Именно это происходит в методе pop() в примере:
// Извлекаем из rbtree контейнера его 'ноду' (tree-node)
auto node = values_.extract(values_.begin());
// Теперь values_ не содрежит в себе первого элемента, этот элемент полностью переехал в node
// values_mutex_ синхронизирует доступ к values_. раз мы вынули из этого контейнера
// интересующую нас ноду, для дальнейшей работы с нодой нет необходимости держать блокировку.
lock.unlock();
// Наружу нам необходимо вернуть только элемент, а не всю ноду. Делаем std::move элемента из ноды.
return std::move(node.value());
// здесь вызовется деструктор для ноды
Таким образом наша многопоточная очередь в C++17 стала:
- более производительной — за счёт уменьшения количества динамических аллокаций и уменьшения времени, которое программа проводит в критической секции;
- более безопасной — за счёт уменьшения количества мест, кидающих исключения, и за счет меньшего количества аллокаций;
- менее требовательной к памяти.
Автоматическое определение шаблонных параметров для классов
В черновик C++17 добавили автоматическое определение шаблонных параметров для шаблонных классов. Это значит, что простые шаблонные классы, конструктор которых явно использует шаблонный параметр, теперь автоматически определяют свой тип:
Было | Стало |
---|---|
|
|
|
|
|
|
std::string_view
Продолжаем эксурс в дивный мир C++17. Давайте посмотрим на следующую C++11 функцию, печатающую сообщение на экран:
// C++11
#include <string>
void get_vendor_from_id(const std::string& id) { // аллоцирует память, если большой массив символов передан на вход вместо std::string
std::cout <<
id.substr(0, id.find_last_of(':')); // аллоцирует память при создании больших подстрок
}
// TODO: дописать get_vendor_from_id(const char* id) чтобы избавиться от динамической аллокации памяти
В C++17 можно написать лучше:
// C++17
#include <string_view>
void get_vendor_from_id(std::string_view id) { // не аллоцирует память, работает с `const char*`, `char*`, `const std::string&` и т.д.
std::cout <<
id.substr(0, id.find_last_of(':')); // не аллоцирует память для подстрок
}
std::basic_string_view или std::string_view — это класс, не владеющий строкой, но хранящий указатель на начало строки и её размер. Класс пришел в стандарт из Boost, где он назывался boost::basic_string_ref или boost::string_ref.
std::string_view уже давно обосновался в черновиках C++17, однако именно на последнем собрании было решено исправить его взимодействие с std::string. Теперь файл <string_view> может не подключать файл , за счет чего использование std::string_view становится более легковесным и компиляция программы происходит немного быстрее.
Рекомендации:
- используйте единственную функцию, принимающую string_view, вместо перегруженных функций, принимающих const std::string&, const char* и т.д.;
- передавайте string_view по копии (нет необходимости писать `const string_view& id`, достаточно просто `string_view id`).
Осторожно: string_view не гарантирует, что строчка, которая в нем хранится, оканчивается на символ '', так что не стоит использовать функции наподобие string_view::data() в местах, где необходимо передавать нуль-терминированные строчки.
if (init; condition)
Давайте рассмотрим следующий пример функции с критической секцией:
// C++11
void foo() {
// ...
{
std::lock_guard<std::mutex> lock(m);
if (!container.empty()) {
// do something
}
}
// ...
}
Многим людям такая конструкция не нравилась, пустые скобки выглядят не очень красиво. Поэтому в C++17 решено было сделать всё красивее:
// C++17
void foo() {
// ...
if (std::lock_guard lock(m); !container.empty()) {
// do something
}
// ...
}
В приведенном выше примере переменная lock будет существовать до закрывающей фигурной скобки оператора if.
Structured bindings
std::set<int> s;
// ...
auto [it, ok] = s.insert(42);
// Теперь it — интегратор на вставленный элемент; ok - bool переменная с результатом.
if (!ok) {
throw std::logic_error("42 is already in set");
}
s.insert(it, 43);
// ...
Structured bindings работает не только с std::pair или std::tuple, а с любыми структурами:
struct my_struct { std::string s; int i; };
my_struct my_function();
// ...
auto [str, integer] = my_function();
А ещё...
В C++17 так же есть:
- синтаксис наподобие template <auto I> struct my_class{ /*… */ };
- filesystem — классы и функции для кросплатформенной работы с файловой системой;
- std::to_chars/std::from_chars — методы для очень быстрых преобразований чисел в строки и строк в числа с использованием C локали;
- std::has_unique_object_representations <T> — type_trait, помогающий определять «уникальную-представимость» типа в бинарном виде;
- new для типов с alignment большим, чем стандартный;
- inline для переменных — если в разных единицах трансляции присутствует переменная с внешней линковкой с одним и тем же именем, то оставить и использовать только одну переменную (без inline будет ошибка линковки);
- std::not_fn коректно работающий с operator() const&, operator() && и т.д.;
- зафиксирован порядок выполнения некоторых операций. Например, если есть выражение, содержащее =, то сначала выполнится его правая часть, потом — левая;
- гарантированный copy elision;
- огромное количество математических функций;
- std::string::data(), возвращающий неконстантый char* (УРА!);
- constexpr для итераторов, std::array и вспомогательных функций (моя фишечка :);
- явная пометка старья типа std::iterator, std::is_literal_type, std::allocator<void>, std::get_temporary_buffer и т.д. как deprecated;
- удаление функций, принимающих аллокаторы из std::function;
- std::any — класс для хранения любых значений;
- std::optional — класс, хранящий определенное значение, либо флаг, что значения нет;
- fallthrough, nodiscard, maybe_unused;
- constexpr лямбды;
- лямбды с [*this]( /*… */ ){ /*… */ };
- полиморфные алокаторы — type-erased алокаторы, отличное решение, чтобы передавать свои алокаторы в чужие библиотеки;
- lock_guard, работающий сразу со множеством мьютексов;
- многое другое.
Напоследок
На конференции C++Siberia в августе мы постараемся рассказать о новинках С++ с большим количеством примеров, объяснить, почему именно такой дизайн был выбран, ответим на ваши вопросы и упомянем множество других мелочей, которые невозможно поместить в статью.
Автор: Яндекс