Останні новини про розвиток C++

Нещодавно у фінському місті Оулу завершилася зустріч міжнародної робочої групи 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++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> може не підключати файл <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<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 у серпні ми постараємося розповісти про новинки с++ З великою кількістю прикладів, пояснити, чому саме такий дизайн був обраний, відповімо на ваші питання і згадаємо безліч інших дрібниць, які неможливо помістити в статтю.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.