C++ Russia: як це було

Якщо на початку п'єси ви кажете, що на стіні висить код С++, то до кінця він повинен неодмінно вистрілити вам в ногу.

Б'ярне Строуструп

З 31-го жовтня до 1-го листопада у Петербурзі пройшла конференція C++ Russia Piter – одне з масштабних конференцій з програмування у Росії, організована JUG Ru Group. Серед запрошених спікерів – члени комітету зі стандартизації C++, доповідачі з CppCon, автори книг видавництва O'Reilly та мейнтейнери таких проектів, як LLVM, libc++ та Boost. Конференція орієнтована досвідчених розробників на C++, бажаючих поглибити свою експертизу та обмінятися досвідом у живому спілкуванні. Студентам, аспірантам та викладачам університетів надаються дуже приємні знижки.

Московське видання конференції можна буде відвідати вже у квітні наступного року, а поки що наші студенти розкажуть, що цікавого вони дізналися на минулому заході. 

C++ Russia: як це було

фото з альбому конференції

Про нас

Над цією посадою працювали двоє студентів НДУ ВШЕ - Санкт-Петербург:

  • Ліза Василенко – студентка 4-го курсу бакалаврату, яка вивчає напрямок «Мови програмування» в рамках програми «Прикладна математика та інформатика». Познайомившись із мовою C++ на першому курсі університету, згодом набула досвіду роботи з ним на стажуваннях в індустрії. Захоплення мовами програмування загалом та функціональним програмуванням, зокрема, наклало відбиток на вибір доповідей на конференції.
  • Даня Смирнов – студент 1-го курсу магістратури «Програмування та аналіз даних». Ще в школі писав на C++ олімпіадні завдання, а далі якось так вийшло, що мова постійно випливала у навчальній діяльності і в результаті стала основним робітником. У конференції вирішив взяти участь, щоб підтягнути свої знання, а також дізнатися про нові можливості.

У розсилці керівництво факультету часто ділиться інформацією про освітні події, пов'язані з нашою спеціальністю. У вересні ми побачили інформацію про C++ Russia і вирішили зареєструватися як слухачі. Це наш перший досвід участі в подібних конференціях.

Структура конференції

  • Доповіді

Протягом двох днів експерти прочитали 30 доповідей, висвітливши багато гарячих топиків: дотепні застосування фіч мови для вирішення прикладних завдань, майбутні оновлення мови у зв'язку з новим стандартом, компроміси при дизайні C++ та запобіжні заходи при роботі з їх наслідками, приклади цікавої архітектури проектів, а також деякі деталі підкапотні інфраструктури мови. Одночасно проходило по 3 виступи, найчастіше два російською та одне англійською мовою.

  • Discussion zones

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

  • Lightning Talks та неформальні дискусії

Якщо захотілося зробити коротку доповідь — можна записатися на дошці на вечірній Lightning Talk і отримати п'ять хвилин часу на розповідь про що завгодно за темою конференції. Наприклад, швидке введення в sanitizers для C++ (для деяких виявилося в новинку) або історія про баг у генерації синусоїди, який можна тільки почути, але не побачити.

Інший формат – панельна дискусія «З комітетом до душі». На сцені — деякі члени комітету зі стандартизації, на проекторі — камін (офіційно — для створення душевної атмосфери, але причина «бо ВСЕ В ВОГНІ» здається кумеднішою), питання — про стандарт та загальне бачення C++, без бурхливих технічних обговорень і холіварів. Виявилося, що у комітеті теж сидять живі люди, які можуть бути у чомусь не до кінця впевнені чи чогось не знати.

Для любителів холіварів у справі залишався третій захід – BOF-сесія Go проти C++. Беремо любителя Go, любителя C++, перед початком сесії вони разом готують 100500 слайдів на тему (на кшталт проблем з пакетами в C++ або відсутністю дженериків у Go), а потім вони жваво дискутують між собою та із залом, а зал намагається зрозуміти відразу дві точки зору . Якщо починається холівар не по ділу - втручається модератор і примиряє сторони. Такий формат затягує: за кілька годин після початку було пройдено лише половину слайдів. Кінець довелося дуже прискорювати.

  • Стенди партнерів

У холах було представлено партнерів конференції — на стендах розповідали про поточні проекти, пропонували стажування та працевлаштування, проводили квізи та невеликі змагання, а також розігрували приємні призи. При цьому деякі компанії навіть пропонували пройти початкові етапи співбесід, що може бути корисним для тих, хто приїхав не лише слухати доповіді.

Технічні подробиці доповідей

Ми слухали доповіді обидва дні. Часом було важко вибрати одну доповідь з тих, хто паралельно йшов – ми домовилися розділятися та обмінюватися отриманими знаннями у перервах. І навіть так, здається, що багато залишилося втрачено. Тут ми хотіли б розповісти про зміст деяких доповідей, які здалися нам найцікавішими.

Винятки у C++ через призму компіляторних оптимізацій, Роман Русяєв

C++ Russia: як це було
Слайд із презентації

Як відомо з назви, Роман розглянув роботу з винятками з прикладу LLVM. При цьому для Clang, що не використовують у своїй роботі, доповідь все одно може дати деяке уявлення про те, як код потенційно може бути оптимізований. Це так, тому що розробники компіляторів та відповідних стандартних бібліотек спілкуються між собою і багато вдалих рішень можуть збігатися.

Отже, для обробки виняток потрібно зробити безліч дій: викликати код обробки (якщо є) або звільнити ресурси на поточному рівні та розкрутити стек вище. Все це веде до того, що для потенційно виключених викликів компілятор додає додаткові інструкції. Тому якщо виняток за фактом не буде викликано, програма все одно виконуватиме непотрібні дії. Щоб якось знизити накладні витрати, в LLVM є кілька евристик визначення ситуацій, де код обробки винятків додавати не потрібно чи можна знизити кількість «зайвих» інструкцій.

Доповідач розглядає близько десятка їх і показує як ситуації, де вони допомагають прискорити виконання програми, і ті, де ці методи не застосовні.

Таким чином Роман Русяєв підводить слухачів до висновку, що код, який містить роботу з винятками, далеко не завжди можна виконувати з нульовими накладними витратами, і дає такі поради:

  • при розробці бібліотек варто відмовитися від винятків у принципі;
  • якщо винятки все ж таки потрібні, то по можливості скрізь варто додавати модифікатори noexcept (і const), щоб компілятор міг соптимизувати якнайбільше.

Загалом доповідач підтвердив думку, що винятки найкраще використовувати за мінімумом або взагалі від них відмовитися.

Слайди доповіді доступні за посиланням: [«Виключення C++ через призму компіляторних оптимізації LLVM»]

Generators, coroutines й інші brain-unrolling sweetness, Adi Shavit

C++ Russia: як це було
Слайд із презентації

Одна з багатьох доповідей цієї конференції, присвячених нововведенням C++20, запам'яталася не лише барвисто оформленою презентацією, але й чітким позначенням проблем з логікою обробки колекцій (цикл for, callback-і).

Adi Shavit виділяє наступні: існуючі зараз способи проходять колекцію повністю і при цьому не дають доступу до деякого внутрішнього проміжного стану (або дають у випадку callback-ів, але з великою кількістю неприємних побічних ефектів, типу того ж Callback Hell). Здавалося б, є ітератори, але і з ними все не так гладко: немає спільних точок входу та виходу (begin → end проти rbegin → rend і так далі), незрозуміло, скільки взагалі ми ітеруватимемося? Починаючи з C++20 ці проблеми вирішуються!

Перший варіант: Ranges. За рахунок обгортки поверх ітераторів ми отримуємо загальний інтерфейс для початку та кінця ітерації, а також отримуємо можливість композиції. Все це дозволяє легко будувати повноцінні конвеєри обробки даних. Але не все так гладко: частина логіки обчислень знаходиться всередині реалізації конкретного ітератора, що може ускладнити код для сприйняття та налагодження.

C++ Russia: як це було
Слайд із презентації

Що ж, на цей випадок до C++20 додані корутини (функції, поведінка яких схожа на генератори в мові Python): виконання можна відкласти, повернувши деяке поточне значення із збереженням при цьому проміжного стану. Таким чином, ми досягаємо не тільки роботи з даними в міру їх появи, але й інкапсулюємо всю логіку всередині конкретної корутини.

Але є ложка дьогтю: на даний момент вони лише частково підтримуються наявними компіляторами, а також реалізовані не так акуратно, як хотілося б: наприклад, поки не варто використовувати в корутинах посилання та тимчасові об'єкти. Плюс, є деякі обмеження щодо того, що може бути корутинами, і constexpr-функції, конструктори/деструктори, а також main у цей список не входять.

Таким чином, корутини вирішують помітну частину проблем із простотою логіки обробки даних, але їх поточні реалізації вимагають доопрацювання.

Матеріали:

C++ трюки з Яндекс.Таксі, Антон Полухін

У своїй професійній діяльності іноді доводиться реалізовувати суто допоміжні штуки: обгортку між внутрішнім інтерфейсом та API якоїсь бібліотеки, логування чи парсинг. При цьому зазвичай немає потреби в якійсь додатковій оптимізації. Але що, якщо ці компоненти використовуються в одних із найпопулярніших у Рунеті сервісах? У такій ситуації доведеться обробляти терабайти за годину самих лише логів! Тоді кожна мілісекунда на рахунку і тому доводиться вдаватися до різних трюків — про них розповідав Антон Полухін.

Мабуть, найцікавішим прикладом була реалізація патерну pointer-to-implementation (pimpl). 

#include <third_party/json.hpp> //PROBLEMS! 
struct Value { 
    Value() = default; 
    Value(Value&& other) = default; 
    Value& operator=(Value&& other) = default; 
    ~Value() = default; 

    std::size_t Size() const { return data_.size(); } 

private: 
    third_party::Json data_; 
};

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

Добре, перенесли #include в .cpp-файл: потрібен forward-declaration оберненого API, а також std:: unique_ptr. Тепер у нас динамічні алокації та інші неприємні речі на кшталт розкиданих по купі даних та знижених гарантій. З цим може допомогти std::aligned_storage. 

struct Value { 
// ... 
private: 
    using JsonNative = third_party::Json; 
    const JsonNative* Ptr() const noexcept; 
    JsonNative* Ptr() noexcept; 

    constexpr std::size_t kImplSize = 32; 
    constexpr std::size_t kImplAlign = 8; 
    std::aligned_storage_t<kImplSize, kImplAlign> data_; 
};

Єдина проблема: потрібно для кожної обгортки прописувати розмір та вирівнювання – зробимо наш pimpl шаблонним з параметрами , використовуємо з будь-якими довільними значення і додамо в деструктор перевірку, що ми все вгадали: 

~FastPimpl() noexcept { 
    validate<sizeof(T), alignof(T)>(); 
    Ptr()->~T(); 
}

template <std::size_t ActualSize, std::size_t ActualAlignment>
static void validate() noexcept { 
    static_assert(
        Size == ActualSize, 
        "Size and sizeof(T) mismatch"
    ); 
    static_assert(
        Alignment == ActualAlignment, 
        "Alignment and alignof(T) mismatch"
    ); 
}

Так як при обробці деструктора T вже визначено, цей код розбиратиметься коректно і на стадії компіляції у вигляді помилок виведе потрібні значення розміру та вирівнювання, які потрібно вписати. Таким чином, ціною одного додаткового запуску компіляції ми позбавляємося динамічної алокації класів, що обертаються, ховаємо API в .cpp-файл з реалізацією, а також отримуємо більш придатну для кешування процесором конструкцію.

Логування та парсинг здалися менш вражаючими, а тому в цьому огляді згадані не будуть.

Слайди доповіді доступні за посиланням: [«C++ трюки з Таксі»]

Modern techniques for keeping your code DRY, Björn Fahller

У цій доповіді Björn Fahller показує кілька різних способів боротьби з таким стилістичним недоліком, як повторювані перевірки умов:

assert(a == IDLE || a == CONNECTED || a == DISCONNECTED);

Знайомо? Використовуючи кілька потужних технік С++, що з'явилися в недавніх стандартах, можна витончено реалізувати ту ж функціональність без втрат продуктивності. Порівняйте:   

assert(a == any_of(IDLE, CONNECTED, DISCONNECTED));

Для обробки нефіксованого числа перевірок відразу проситься використати variadic templates і fold expressions. Припустимо, що хочемо перевірити рівність кількох змінних елементу enum'a state_type. Перше, що спадає на думку – написати допоміжну функцію is_any_of:


enum state_type { IDLE, CONNECTED, DISCONNECTED };

template <typename ... Ts>
bool is_any_of(state_type s, const Ts& ... ts) { 
    return ((s == ts) || ...); 
}

Такий проміжний результат викликає розчарування. Поки що код читання не стає:

assert(is_any_of(state, IDLE, DISCONNECTING, DISCONNECTED)); 

Небагато виправити ситуацію допоможуть non-type template parameters. З їх допомогою перенесемо перераховані елементи enum'a до списку параметрів шаблону: 

template <state_type ... states>
bool is_any_of(state_type t) { 
    return ((t == states) | ...); 
}
	
assert(is_any_of<IDLE, DISCONNECTING, DISCONNECTED>(state)); 

З використанням auto в не типовому параметрі шаблону (C++17), підхід просто узагальнюється на порівняння не тільки з елементами state_type, але і з примітивними типами, які можна використовувати як non-type template parameters:


template <auto ... alternatives, typename T>
bool is_any_of(const T& t) {
    return ((t == alternatives) | ...);
}

Шляхом таких послідовних поліпшень досягається бажаний синтаксис для перевірок:


template <class ... Ts>
struct any_of : private std::tuple<Ts ...> { 
// поленимся и унаследуем конструкторы от tuple 
        using std::tuple<Ts ...>::tuple;
        template <typename T>
        bool operator ==(const T& t) const {
                return std::apply(
                        [&t](const auto& ... ts) {
                                return ((ts == t) || ...);
                        },
                        static_cast<const std::tuple<Ts ...>&>(*this));
        }
};

template <class ... Ts>
any_of(Ts ...) -> any_of<Ts ... >;
 
assert(any_of(IDLE, DISCONNECTING, DISCONNECTED) == state);

У цьому прикладі deduction guide служить для підказки бажаних шаблонних параметрів структури компілятор, знає типи аргументів конструктора. 

Далі – цікавіше. Бьорн вчить узагальнювати код для операторів порівняння крім ==, а потім і для довільних операцій. Принагідно з прикладу використання пояснюються такі фічі як no_unique_address attribute (C++20) і шаблонні параметри в лямбда-функціях (C++20). (Так, тепер синтакс лямбд ще легше запам'ятати – це чотири послідовні пари дужок всіх сортів.) Підсумкове рішення з використанням функцій як деталей конструктора особисто мені дуже гріє душу, не кажучи вже про вираз tuple у кращих традиціях лямбда-обчислення.

Наприкінці не забуваємо навести лоск:

  • Згадаймо, що лямбди – constexpr безкоштовно; 
  • Додамо perfect forwarding і подивимося на його потворний синтакс стосовно parameter pack у замиканні лямбд;
  • Дамо компілятору більше можливостей для оптимізації з conditional noexcept; 
  • Подбаємо про більш зрозумілий висновок помилок у шаблонах завдяки явним значенням лямбд, що повертаються. Це змусить компілятор робити більше перевірок до виклику шаблонної функції – на стадії перевірки типів. 

За подробицями звертайтесь до матеріалів лекції: 

Наші враження

Наша перша участь у C++ Russia запам'яталася своєю насиченістю. Склалося враження про С++ Russia як про душевний захід, де межа між навчанням і живим спілкуванням майже не відчутна. Все, від настрою доповідачів до конкурсів від партнерів заходу, спонукає до бурхливих обговорень. Змістовна частина конференції, що полягає у доповідях, охоплює досить широкий спектр тем, включаючи нововведення С++, приклади з практики великих проектів та ідеологічні архітектурні міркування. Але було б несправедливо обділити увагою і соціальну складову заходи, що сприятиме подоланню мовних бар'єрів щодо не лише С++.

Дякуємо організаторам конференції за можливість взяти участь у такій події!
Пост організаторів про минуле, сьогодення та майбутнє C++ Russia ви могли бачити у блозі JUG Ru.

Дякую за прочитання, і сподіваємося, що наш переказ подій виявився корисним!

Джерело: habr.com

Додати коментар або відгук