C++ Rosja: jak to się stało

Jeśli na początku zabawy powiesz, że na ścianie wisi kod C++, to na koniec strzeli ci on w stopę.

Bjarne’a Stroustrupa

W dniach 31 października - 1 listopada w St. Petersburgu odbyła się konferencja C++ Russia Piter - jedna z dużych konferencji programistycznych w Rosji, organizowana przez Grupę JUG Ru. Zaproszeni prelegenci to członkowie Komitetu ds. Standardów C++, prelegenci CppCon, autorzy książek O'Reilly i opiekunowie projektów takich jak LLVM, libc++ i Boost. Konferencja skierowana jest do doświadczonych programistów C++, którzy chcą pogłębić swoją wiedzę i wymienić doświadczenia w komunikacji na żywo. Studenci, doktoranci i nauczyciele akademiccy mogą liczyć na bardzo atrakcyjne zniżki.

Moskiewską edycję konferencji będzie można zwiedzać już w kwietniu przyszłego roku, a tymczasem nasi studenci opowiedzą Wam, czego ciekawego nauczyli się na ostatnim wydarzeniu. 

C++ Rosja: jak to się stało

Zdjęcie z album konferencyjny

O nas

Nad tym stanowiskiem pracowało dwóch studentów z Państwowej Wyższej Szkoły Ekonomicznej w St. Petersburgu:

  • Liza Wasilenko jest studentką IV roku studiów licencjackich studiujących Języki Programowania w ramach programu Matematyki Stosowanej i Informatyki. Po zapoznaniu się z językiem C++ na pierwszym roku studiów, doświadczenie w pracy z nim zdobywałem później poprzez staże w branży. Moja pasja do języków programowania w ogóle, a programowania funkcjonalnego w szczególności odcisnęła piętno na wyborze referatów na konferencji.
  • Danya Smirnov jest studentką pierwszego roku studiów magisterskich „Programowanie i analiza danych”. Jeszcze w szkole pisałem zadania olimpijskie w C++ i wtedy jakoś tak się złożyło, że język ten stale pojawiał się w działaniach edukacyjnych i ostatecznie stał się głównym językiem roboczym. Zdecydowałem się na udział w konferencji, aby pogłębić swoją wiedzę, a także poznać nowe możliwości.

W biuletynie kierownictwo wydziału często dzieli się informacjami o wydarzeniach edukacyjnych związanych z naszą specjalnością. We wrześniu zobaczyliśmy informację o C++ Rosja i postanowiliśmy zarejestrować się jako słuchacze. To nasze pierwsze doświadczenie udziału w tego typu konferencjach.

Struktura konferencji

  • Raporty

W ciągu dwóch dni eksperci przeczytali 30 raportów poruszających wiele gorących tematów: pomysłowe wykorzystanie funkcji językowych do rozwiązywania stosowanych problemów, nadchodzące aktualizacje językowe w związku z nowym standardem, kompromisy w projektowaniu C++ i środki ostrożności podczas pracy z ich konsekwencjami, przykłady ciekawej architektury projektu, a także kilka ukrytych szczegółów infrastruktury językowej. Jednocześnie odbywały się trzy przedstawienia, najczęściej dwa w języku rosyjskim i jeden w języku angielskim.

  • Strefy dyskusji

Po wystąpieniu wszystkie niezadane pytania i niedokończone dyskusje zostały przeniesione do specjalnie wyznaczonych miejsc do komunikacji z prelegentami, wyposażonych w tablice markerowe. Dobry sposób na umilenie sobie przerwy między wystąpieniami przyjemną rozmową.

  • Błyskawiczne rozmowy i nieformalne dyskusje

Jeśli chcesz wygłosić krótki raport, możesz zapisać się na tablicy na wieczorny Lightning Talk i zyskać pięć minut na rozmowę na dowolny temat związany z konferencją. Na przykład szybkie wprowadzenie do środków dezynfekujących dla C++ (dla niektórych było to nowością) lub opowieść o błędzie w generowaniu fali sinusoidalnej, który można tylko usłyszeć, ale nie widać.

Innym formatem jest dyskusja panelowa „Z Komitetem Od Serca do Serca”. Na scenie niektórzy członkowie komitetu normalizacyjnego, na projektorze kominek (oficjalnie - dla stworzenia szczerej atmosfery, ale powód „bo WSZYSTKO PALI SIĘ” wydaje się zabawniejszy), pytania o standard i ogólną wizję C++ , bez gorących dyskusji technicznych i holiwarów. Okazało się, że w komisji zasiadają także żywi ludzie, którzy być może nie są czegoś do końca pewni lub czegoś nie wiedzą.

Dla fanów holivarów pozostało w temacie trzecie wydarzenie – sesja BOF „Go vs. C++”. Bierzemy miłośnika Go, miłośnika C++, przed rozpoczęciem sesji wspólnie przygotowują 100500 XNUMX slajdów na dany temat (np. problemy z pakietami w C++ lub brak generycznych w Go), a następnie toczą między sobą ożywioną dyskusję i z publicznością, a ona stara się zrozumieć dwa punkty widzenia na raz. Jeżeli holiwar zaczyna się wyrwany z kontekstu, moderator interweniuje i godzi strony. Ten format uzależnia: kilka godzin od rozpoczęcia ukończono tylko połowę slajdów. Końcówkę trzeba było mocno przyspieszyć.

  • Partner stoi

Partnerzy konferencji byli reprezentowani w salach - na stoiskach opowiadali o bieżących projektach, oferowali staże i zatrudnienie, organizowali quizy i małe konkursy, a także losowali atrakcyjne nagrody. Jednocześnie niektóre firmy oferowały nawet przejście przez początkowe etapy rozmów kwalifikacyjnych, co mogło być przydatne dla tych, którzy przyszli nie tylko słuchać raportów.

Szczegóły techniczne raportów

W oba dni słuchaliśmy relacji. Momentami trudno było wybrać jeden raport spośród równoległych – zgodziliśmy się na rozdzielenie i wymianę wiedzy zdobytej podczas przerw. A mimo to wydaje się, że wiele zostało pominiętych. W tym miejscu chcielibyśmy omówić treść niektórych raportów, które uznaliśmy za najciekawsze

Wyjątki w C++ przez pryzmat optymalizacji kompilatorów, Roman Rusyaev

C++ Rosja: jak to się stało
Przesuń z презентации

Jak sugeruje tytuł, Roman przyjrzał się pracy z wyjątkami na przykładzie LLVM. Jednocześnie dla tych, którzy nie korzystają z Clanga w swojej pracy, raport może mimo wszystko dać pewne wyobrażenie o tym, w jaki sposób można by potencjalnie zoptymalizować kod. Dzieje się tak dlatego, że twórcy kompilatorów i odpowiednich bibliotek standardowych komunikują się ze sobą i wiele udanych rozwiązań może się pokrywać.

Aby więc obsłużyć wyjątek, musisz zrobić wiele rzeczy: wywołać kod obsługi (jeśli istnieje) lub zwolnić zasoby na bieżącym poziomie i zwiększyć stos wyżej. Wszystko to prowadzi do tego, że kompilator dodaje dodatkowe instrukcje dla wywołań, które potencjalnie zgłaszają wyjątki. Dlatego też, jeśli wyjątek faktycznie nie zostanie zgłoszony, program i tak będzie wykonywał niepotrzebne akcje. Aby w jakiś sposób zmniejszyć obciążenie, LLVM posiada kilka heurystyk pozwalających określić sytuacje, w których nie trzeba dodawać kodu obsługi wyjątków lub można zmniejszyć liczbę „dodatkowych” instrukcji.

Prelegent omawia kilkanaście z nich i pokazuje zarówno sytuacje, w których pomagają one przyspieszyć wykonanie programu, jak i takie, w których metody te nie mają zastosowania.

W ten sposób Roman Rusjajew prowadzi uczniów do wniosku, że kod zawierający obsługę wyjątków nie zawsze może zostać wykonany przy zerowym nakładzie pracy i daje następującą radę:

  • rozwijając biblioteki, warto w zasadzie porzucić wyjątki;
  • jeśli nadal potrzebne są wyjątki, to jeśli to możliwe, warto dodać wszędzie modyfikatory noexcept (i const), aby kompilator mógł zoptymalizować tak bardzo, jak to możliwe.

Generalnie prelegent potwierdził pogląd, że wyjątki najlepiej stosować do minimum lub w ogóle z nich zrezygnować.

Slajdy z raportami dostępne są pod poniższym linkiem: [„Wyjątki C++ przez pryzmat optymalizacji kompilatora LLVM”]

Generatory, współprogramy i inne słodycze rozwijające mózg, Adi Shavit

C++ Rosja: jak to się stało
Przesuń z презентации

Jeden z wielu raportów na tej konferencji poświęcony innowacjom w C++20 zapadł w pamięć nie tylko ze względu na barwną prezentację, ale także na jasną identyfikację istniejących problemów z logiką przetwarzania kolekcji (pętla for, wywołania zwrotne).

Adi Shavit podkreśla, co następuje: obecnie dostępne metody przechodzą przez całą kolekcję i nie zapewniają dostępu do jakiegoś wewnętrznego stanu pośredniego (lub dają w przypadku wywołań zwrotnych, ale z dużą liczbą nieprzyjemnych skutków ubocznych, takich jak Callback Hell) . Wydawałoby się, że istnieją iteratory, ale nawet z nimi nie wszystko jest tak gładkie: nie ma wspólnych punktów wejścia i wyjścia (begin → end kontra rbegin → rend itd.), nie jest jasne, jak długo będziemy iterować? Począwszy od C++ 20, te problemy zostały rozwiązane!

Opcja pierwsza: zakresy. Zawijając iteratory, uzyskujemy wspólny interfejs dla początku i końca iteracji, a także uzyskujemy możliwość komponowania. Wszystko to ułatwia budowanie pełnoprawnych potoków przetwarzania danych. Ale nie wszystko jest takie gładkie: część logiki obliczeniowej znajduje się wewnątrz implementacji konkretnego iteratora, co może komplikować kod w zrozumieniu i debugowaniu.

C++ Rosja: jak to się stało
Przesuń z презентации

Cóż, w tym przypadku C++20 dodał współprogramy (funkcje, których zachowanie jest podobne do generatorów w Pythonie): wykonanie można odłożyć, zwracając pewną bieżącą wartość, zachowując jednocześnie stan pośredni. W ten sposób osiągamy nie tylko pracę z danymi w postaci, w jakiej się pojawiają, ale także zamykamy całą logikę w określonej współprogramie.

Ale jest w tym pewien błąd: w tej chwili są one tylko częściowo obsługiwane przez istniejące kompilatory i nie są też zaimplementowane tak starannie, jak byśmy chcieli: na przykład nie warto jeszcze używać referencji i obiektów tymczasowych w współprogramach. Ponadto istnieją pewne ograniczenia dotyczące tego, co może być współprogramem, a funkcje constexpr, konstruktory/destruktory i main nie są uwzględnione na tej liście.

Zatem współprogramy rozwiązują znaczną część problemów poprzez prostotę logiki przetwarzania danych, ale ich obecne implementacje wymagają udoskonalenia.

Materiały:

Sztuczki C++ od Yandex.Taxi, Anton Polukhin

W swojej działalności zawodowej czasami muszę wdrażać rzeczy czysto pomocnicze: wrapper pomiędzy interfejsem wewnętrznym a API jakiejś biblioteki, logowanie czy parsowanie. W takim przypadku zwykle nie ma potrzeby dodatkowej optymalizacji. Ale co, jeśli te komponenty są używane w niektórych z najpopularniejszych usług w RuNet? W takiej sytuacji będziesz musiał przetworzyć same terabajty logów na godzinę! Wtedy liczy się każda milisekunda i dlatego trzeba uciekać się do różnych trików – mówił o nich Anton Polukhin.

Być może najciekawszym przykładem była implementacja wzorca wskaźnika do implementacji (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_; 
};

W tym przykładzie najpierw chcę pozbyć się plików nagłówkowych bibliotek zewnętrznych - skompiluje się to szybciej, a Ty możesz zabezpieczyć się przed możliwymi konfliktami nazw i innymi podobnymi błędami. 

OK, przenieśliśmy #include do pliku .cpp: potrzebujemy deklaracji forward opakowanego API, a także std::unique_ptr. Teraz mamy alokacje dynamiczne i inne nieprzyjemne rzeczy, takie jak dane rozproszone w wielu danych i ograniczone gwarancje. std::aligned_storage może w tym wszystkim pomóc. 

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_; 
};

Jedyny problem: musisz określić rozmiar i wyrównanie dla każdego opakowania - utwórzmy nasz szablon pimpl z parametrami , użyj dowolnych wartości i dodaj do destruktora sprawdzenie, czy wszystko mamy dobrze: 

~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"
    ); 
}

Ponieważ T jest już zdefiniowane podczas przetwarzania destruktora, kod ten zostanie poprawnie przeanalizowany i na etapie kompilacji wyświetli wymagany rozmiar i wartości wyrównania, które należy wprowadzić jako błędy. Tym samym kosztem jednego dodatkowego przebiegu kompilacji pozbywamy się dynamicznej alokacji opakowanych klas, ukrywamy API w pliku .cpp z implementacją, a także otrzymujemy projekt bardziej odpowiedni do buforowania przez procesor.

Rejestrowanie i analizowanie wydawało się mniej imponujące i dlatego nie zostaną wspomniane w tej recenzji.

Slajdy z raportami dostępne są pod poniższym linkiem: [„Sztuczki C++ z Taxi”]

Nowoczesne techniki utrzymywania kodu w stanie SUCHYM, Björn Fahller

W tym wykładzie Björn Fahller pokazuje kilka różnych sposobów zwalczania błędów stylistycznych związanych z wielokrotną kontrolą stanu:

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

Brzmi znajomo? Używając kilku potężnych technik C++ wprowadzonych w najnowszych standardach, możesz elegancko zaimplementować tę samą funkcjonalność bez żadnego spadku wydajności. Porównywać:   

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

Aby obsłużyć nieustaloną liczbę kontroli, należy natychmiast użyć szablonów variadic i wyrażeń składanych. Załóżmy, że chcemy sprawdzić równość kilku zmiennych z elementem state_type wyliczenia. Pierwszą rzeczą, która przychodzi na myśl, jest napisanie funkcji pomocniczej 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) || ...); 
}

Ten pośredni wynik rozczarowuje. Jak na razie kod nie staje się bardziej czytelny:

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

Parametry szablonu innego typu pomogą nieco poprawić sytuację. Za ich pomocą przeniesiemy przeliczalne elementy wyliczenia na listę parametrów szablonu: 

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

Używając auto w parametrze szablonu innego niż typ (C++ 17), podejście to po prostu uogólnia porównania nie tylko z elementami state_type, ale także z typami pierwotnymi, których można używać jako parametrów szablonu innego niż typ:


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

Dzięki tym kolejnym ulepszeniom osiągnięto pożądaną płynną składnię kontroli:


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);

W tym przykładzie przewodnik dedukcyjny służy do sugerowania kompilatorowi pożądanych parametrów szablonu struktury, który zna typy argumentów konstruktora. 

Dalej - ciekawiej. Bjorn uczy, jak uogólnić powstały kod na operatory porównania wykraczające poza ==, a następnie na dowolne operacje. Po drodze wyjaśniono takie funkcje, jak atrybut no_unique_address (C++ 20) i parametry szablonu w funkcjach lambda (C++ 20) na przykładach użycia. (Tak, teraz składnia lambdy jest jeszcze łatwiejsza do zapamiętania - są to cztery kolejne pary wszelkiego rodzaju nawiasów.) Ostateczne rozwiązanie wykorzystujące funkcje jako szczegóły konstruktora naprawdę rozgrzewa moją duszę, nie mówiąc już o wyrażeniu Tuple w najlepszych tradycjach lambdy rachunek różniczkowy.

Na koniec nie zapomnij go wypolerować:

  • Pamiętaj, że lambdy są constexpr za darmo; 
  • Dodajmy idealne przekazywanie i przyjrzyjmy się jego brzydkiej składni w odniesieniu do pakietu parametrów w zamknięciu lambda;
  • Dajmy kompilatorowi więcej możliwości optymalizacji za pomocą warunkowego noexcept; 
  • Zadbajmy o bardziej zrozumiałe wyjście błędów w szablonach dzięki jawnym zwracanym wartościom lambd. Zmusi to kompilator do wykonania większej liczby kontroli przed faktycznym wywołaniem funkcji szablonu - na etapie sprawdzania typu. 

Szczegóły w materiałach wykładowych: 

Nasze wrażenia

Nasz pierwszy udział w C++ Rosja był niezapomniany ze względu na intensywność. Odniosłem wrażenie, że C++ Rosja to szczere wydarzenie, w którym granica między szkoleniem a komunikacją na żywo jest prawie niezauważalna. Wszystko, od nastroju prelegentów po konkursy ze strony partnerów wydarzenia, sprzyja gorącym dyskusjom. Treść konferencji, składająca się z raportów, obejmuje dość szeroki zakres tematów obejmujący innowacje w C++, studia przypadków dużych projektów i ideologiczne rozważania architektoniczne. Jednak niesprawiedliwym byłoby zignorowanie aspektu społecznościowego wydarzenia, który pomaga pokonać bariery językowe nie tylko w odniesieniu do C++.

Dziękujemy organizatorom konferencji za możliwość wzięcia udziału w takim wydarzeniu!
Być może widziałeś post organizatorów na temat przeszłości, teraźniejszości i przyszłości C++ Rosja na blogu JUG Ru.

Dziękujemy za przeczytanie i mamy nadzieję, że nasze opowiadanie o wydarzeniach było pomocne!

Źródło: www.habr.com

Dodaj komentarz