Последние новости о развитии C++

Недавно в финском городе Оулу завершилась встреча международной рабочей группы WG21 по стандартизации C++, в которой впервые официально участвовали сотрудники Яндекса. На ней утвердили черновой вариант C++17 со множеством новых классов, методов и полезных нововведений языка.

6e71a380253b447ebb1da8925766caaf.png

Во время поездки мы обедали с Бьярне Страуструпом, катались в лифте с Гербом Саттером, жали руку Беману Дейвсу, выходили «подышать воздухом» с Винцентом Боте, обсуждали онлайн-игры с Гором Нишановым, были на приёме в мэрии Оулу и общались с мэром. А ещё мы вместе со всеми с 8:30 до 17:30 работали над новым стандартом C++, зачастую собираясь в 20:00 чтобы ещё четыре часика поработать и успеть добавить пару хороших вещей.

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

if constexpr (condition)
В C++17 появилась возможность на этапе компиляции выполнять if:
template 
auto rget(const std::pair& p) {
    if constexpr (I == 0) {
        return p.second;
    } else {
        return p.first;
    }
}

При этом неактиваная ветка ветвления не влияет на определение возвращаемого значения. Другими словами, данный пример скомпилируется и:
  • при вызове rget<0>(std: pair{}) тип возвращаемого значения будет short;
  • при вызове rget<1>(std: pair{}) тип возвращаемого значения будет 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
Позвольте представить: std: variant — union, который помнит что хранит.
std::variant v;
v = "Hello word";
assert(std::get(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>:: type` убран).

Многопоточные алгоритмы
Практически все алгоритмы из заголовочного файла были продублированы в виде версий, принимающих ExecutionPolicy. Теперь, например, можно выполнять алгоритмы многопоточно:
std::vector 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++11
void push(std::multiset&& items) {
    std::unique_lock lock(values_mutex_);
    for (auto&& val : items) {
        // аллоцирует память, может кидать исключения
        values_.insert(val);
    }

    cond_.notify_one();
}

value_type pop() {
    std::unique_lock lock(values_mutex_);
    while (values_.empty()) {
        cond_.wait(lock);
    }

    // аллоцирет память, может кидать исключения
    value_type ret = *values_.begin();
    // деаллоцирует память
    values_.erase(values_.begin());

    return ret;
}
// C++17
void push(std::multiset&& items) {
    std::unique_lock lock(values_mutex_);

    // не аллоцирует память, не кидает исключения.
    // работает намного быстрее (см. #2)
    values_.merge(std::move(items));

    cond_.notify_one();
}

value_type pop() {
    std::unique_lock lock(values_mutex_);
    while (values_.empty()) {
        cond_.wait(lock);
    }

    // не аллоцирет память и не кидает исключения (см. #2)
    auto node = values_.extract(values_.begin());
    lock.unlock();

    // извлекаем значение из ноды multiset'а
    return std::move(node.value());
}

В 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::pair p(17, 42.0);
std::pair p(17, 42.0);
std::lock_guard lck(mut_);
std::lock_guard lck(mut_);
std::lock_guard lck(mut_);
auto lck = std::lock_guard(mut_);

std: string_view
Продолжаем эксурс в дивный мир C++17. Давайте посмотрим на следующую C++11 функцию, печатающую сообщение на экран:
// C++11
#include 
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 
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. Теперь файл может не подключать файл , за счет чего использование std: string_view становится более легковесным и компиляция программы происходит немного быстрее.

Рекомендации:

  • используйте единственную функцию, принимающую string_view, вместо перегруженных функций, принимающих const std: string&, const char* и т.д.;
  • передавайте string_view по копии (нет необходимости писать `const string_view& id`, достаточно просто `string_view id`).

Осторожно: string_view не гарантирует, что строчка, которая в нем хранится, оканчивается на символ '\0', так что не стоит использовать функции наподобие string_view: data () в местах, где необходимо передавать нуль-терминированные строчки.if (init; condition)
Давайте рассмотрим следующий пример функции с критической секцией:
// C++11
void foo() {
    // ...
    {
        std::lock_guard 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 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 struct my_class{ /*… */ };
  • filesystem — классы и функции для кросплатформенной работы с файловой системой;
  • std: to_chars/std: from_chars — методы для очень быстрых преобразований чисел в строки и строк в числа с использованием C локали;
  • std: has_unique_object_representations  — 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, std: get_temporary_buffer и т.д. как deprecated;
  • удаление функций, принимающих аллокаторы из std: function;
  • std: any — класс для хранения любых значений;
  • std: optional — класс, хранящий определенное значение, либо флаг, что значения нет;
  • fallthrough, nodiscard, maybe_unused;
  • constexpr лямбды;
  • лямбды с [*this](/*… */){ /*… */ };
  • полиморфные алокаторы — type-erased алокаторы, отличное решение, чтобы передавать свои алокаторы в чужие библиотеки;
  • lock_guard, работающий сразу со множеством мьютексов;
  • многое другое.
Напоследок
На конференции C++Siberia в августе мы постараемся рассказать о новинках С++ с большим количеством примеров, объяснить, почему именно такой дизайн был выбран, ответим на ваши вопросы и упомянем множество других мелочей, которые невозможно поместить в статью.

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

© Habrahabr.ru