C++ Russia: как это было

    Если в начале пьесы вы говорите, что на стене висит код на С++, то к концу он должен непременно выстрелить вам в ногу.

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

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

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



    Фото из альбома конференции

    О нас


    Над этим постом работали двое студентов НИУ ВШЭ — Санкт-Петербург:

    • Лиза Василенко – студентка 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++ через призму компиляторных оптимизаций, Роман Русяев




    Слайд из презентации

    Как понятно из названия, Роман рассмотрел работу с исключениями на примере LLVM. При этом для не использующих в своей работе Clang доклад всё равно может дать некоторое представление о том, как код потенциально может быть оптимизирован. Это так, потому что разработчики компиляторов и соответствующих стандартных библиотек общаются между собой и многие удачные решения могут совпадать.

    Итак, для обработки исключения требуется сделать множество действий: вызвать код обработки (если есть) или освободить ресурсы на текущем уровне и раскрутить стек выше. Всё это ведёт к тому, что для потенциально выдающих исключения вызовов компилятор добавляет дополнительные инструкции. Поэтому если исключение по факту не будет вызвано, программа все равно станет выполнять ненужные действия. Для того, чтобы как-то снизить накладные расходы, в LLVM есть несколько эвристик определения ситуаций, где код обработки исключений добавлять не нужно или можно снизить количество «лишних» инструкций.

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

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

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

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

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

    Generators, coroutines and other brain-unrolling sweetness, Adi Shavit



    Слайд из презентации

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

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

    Первый вариант: ranges. За счёт обёртки поверх итераторов мы получаем общий интерфейс для начала и конца итерации, а также получаем возможность композиции. Всё это позволяет легко строить полноценные конвейеры обработки данных. Но не всё так гладко: часть логики вычислений находится внутри реализации конкретного итератора, что может усложнить код для восприятия и отладки.


    Слайд из презентации

    Что же, на этот случай в 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 шаблонным с параметрами <T,SizeT,AlignmentT>, используем с какими-нибудь произвольными значения и добавим в деструктор проверку, что мы всё угадали: 

    ~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.

    Спасибо за прочтение, и надеемся, что наш пересказ событий оказался полезным!
    Питерская Вышка
    Компания

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое