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

в 16:19, , рубрики: c++, c++ библиотеки, c++11, C++14, c++17, IT-стандарты, standard library, stl, Алгоритмы, Блог компании Яндекс, Компиляторы, метки:

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

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

Во время поездки мы обедали с Бьярне Страуструпом, катались в лифте с Гербом Саттером, жали руку Беману Дейвсу, выходили «подышать воздухом» с Винцентом Боте, обсуждали онлайн-игры с Гором Нишановым, были на приёме в мэрии Оулу и общались с мэром. А ещё мы вместе со всеми с 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++11
void push(std::multiset<value_type>&& items) {
    std::unique_lock<std::mutex> lock(values_mutex_);
    for (auto&& val : items) {
        // аллоцирует память, может кидать исключения
        values_.insert(val);
    }

    cond_.notify_one();
}

value_type pop() {
    std::unique_lock<std::mutex> 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<value_type>&& items) {
    std::unique_lock<std::mutex> lock(values_mutex_);

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

    cond_.notify_one();
}

value_type pop() {
    std::unique_lock<std::mutex> 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<int, double> p(17, 42.0);
std::pair p(17, 42.0);
std::lock_guard<std::shared_timed_mutex> lck(mut_);
std::lock_guard lck(mut_);
std::lock_guard<std::shared_timed_mutex> lck(mut_);
auto lck = std::lock_guard(mut_);

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 в августе мы постараемся рассказать о новинках С++ с большим количеством примеров, объяснить, почему именно такой дизайн был выбран, ответим на ваши вопросы и упомянем множество других мелочей, которые невозможно поместить в статью.

Автор: Яндекс

Источник

Поделиться новостью

* - обязательные к заполнению поля