C++ Russia: com'è andata

Se all'inizio della pièce dici che sulla parete c'è un codice C++, alla fine deve necessariamente colpirti al piede.

Bjarne Stroustrup

Dal 31 ottobre al 1 novembre si è tenuta a San Pietroburgo la conferenza C++ Russia Piter, una delle più importanti conferenze di programmazione in Russia, organizzata da JUG Ru Group. Tra i relatori invitati ci sono membri del comitato per la standardizzazione di C++, relatori di CppCon, autori di libri editi da O’Reilly, così come i maintainer di progetti come LLVM, libc++ e Boost. La conferenza è rivolta a sviluppatori esperti in C++ che desiderano approfondire la loro competenza e scambiare esperienze in un dialogo diretto. Gli studenti, i dottorandi e i docenti universitari beneficiano di sconti molto vantaggiosi.

L'edizione moscovita della conferenza si terrà già ad aprile del prossimo anno, ma nel frattempo i nostri studenti racconteranno cosa di interessante hanno appreso durante l'evento. 

C++ Russia: com'è andata

Foto da album della conferenza

Chi siamo

A questo post hanno lavorato due studenti della Higher School of Economics - San Pietroburgo:

  • Liza Vasilenko – studentessa del quarto anno di laurea triennale, che studia 'Linguaggi di programmazione' nel corso di 'Matematica applicata e informatica'. Ha conosciuto il linguaggio C++ nel primo anno di università e ha poi acquisito esperienza lavorando con esso durante tirocini in azienda. La passione per i linguaggi di programmazione in generale e per la programmazione funzionale in particolare ha influenzato la sua scelta dei talk alla conferenza.
  • Danya Smirnov – studente del primo anno di magistrale in 'Programmazione e analisi dei dati'. Fin dai tempi della scuola ha risolto problemi olimpici in C++, e poi, in qualche modo, il linguaggio è emerso costantemente nelle sue attività di studio, diventando infine il suo principale strumento di lavoro. Ha deciso di partecipare alla conferenza per approfondire le proprie conoscenze e scoprire nuove opportunità.

Nella newsletter, la direzione del dipartimento condivide spesso informazioni su eventi educativi legati alla nostra specializzazione. A settembre abbiamo visto informazioni su C++ Russia e abbiamo deciso di registrarci come partecipanti.

Struttura della conferenza

  • Interventi

Durante i due giorni, esperti hanno tenuto 30 presentazioni, trattando molti temi caldi: applicazioni ingegnose delle funzionalità del linguaggio per risolvere problemi pratici, futuri aggiornamenti del linguaggio in vista del nuovo standard, compromessi nel design di C++ e precauzioni per lavorare con le loro conseguenze, esempi di architettura interessante dei progetti, così come alcuni dettagli nascosti dell'infrastruttura del linguaggio. Si sono svolti simultaneamente 3 interventi, di solito due in russo e uno in inglese.

  • Zone di discussione

Dopo le presentazioni, tutte le domande rimaste senza risposta e le discussioni incomplete venivano spostate in apposite zone di comunicazione con i relatori, dotate di lavagne. Un buon modo per trascorrere in modo piacevole una pausa tra gli interventi.

  • Lightning Talks e discussioni informali

Se si desidera fare una breve presentazione, è possibile iscriversi sulla lavagna per il Lightning Talk serale e avere cinque minuti per parlare di qualsiasi argomento legato alla conferenza. Ad esempio, una rapida introduzione ai sanitizers per C++ (per alcuni è stata una novità) oppure la storia di un bug nella generazione di una sinusoide, che si può solo ascoltare, ma non vedere.

Un altro formato era il dibattito 'Con il comitato per rimanere in tema'. Sul palco si trovavano alcuni membri del comitato per la standardizzazione, mentre al proiettore c'era un caminetto (ufficialmente per creare un'atmosfera accogliente, ma la ragione 'perché TUTTO È IN FIAMME' sembra più divertente), le domande riguardavano lo standard e la visione generale di C++, senza discussioni tecniche animate o controversie. È emerso che nel comitato ci sono anche persone vere, che possono non essere sempre completamente sicure o sapere tutto.

Per gli amanti delle controversie rimaneva un terzo evento: la sessione BOF 'Go contro C++'. Prendiamo un amante di Go, un amante di C++, all'inizio della sessione preparano insieme 100500 diapositive su argomenti (come problemi con i pacchetti in C++ o l'assenza di generics in Go), quindi discutono animatamente tra di loro e con il pubblico, mentre il pubblico cerca di capire subito due punti di vista. Se inizia una polemica sui dettagli, interviene un moderatore e riconcilia le parti. Questo formato coinvolge: alcune ore dopo l'inizio, era stata coperta solo metà delle diapositive. La fine ha dovuto essere accelerata.

  • Stand dei partner

Nei corridoi sono stati presentati i partner della conferenza — presso gli stand si raccontava dei progetti attuali, si offrivano tirocini e opportunità di lavoro, si svolgevano quiz e piccole competizioni, oltre a vincere premi piacevoli. Alcune aziende hanno persino proposto di superare le prime fasi dei colloqui, cosa che può rivelarsi utile per chi è venuto qui non solo per ascoltare le relazioni.

Dettagli tecnici delle relazioni

Abbiamo ascoltato le relazioni per entrambi i giorni. A volte è stato difficile scegliere una relazione tra quelle che si svolgevano in parallelo – abbiamo concordato di dividerci e di scambiarci le conoscenze acquisite durante le pause. Anche così, sembra che molto sia stato trascurato. Qui vorremmo raccontare il contenuto di alcune delle relazioni che ci sono sembrate più interessanti.

Eccezioni in C++ attraverso la lente delle ottimizzazioni del compilatore, Roman Rusyaev

C++ Russia: com'è andata
Slide da una presentazione

Come suggerisce il titolo, Roman ha esaminato la gestione delle eccezioni attraverso LLVM. Tuttavia, anche per coloro che non utilizzano Clang, la relazione può fornire alcune indicazioni su come il codice possa potenzialmente essere ottimizzato. Questo perché gli sviluppatori di compilatori e relative librerie standard si confrontano e molte soluzioni vincenti possono essere simili.

Pertanto, per gestire le eccezioni è necessario compiere molteplici azioni: chiamare il codice di gestione (se presente) o liberare le risorse a livello attuale e far risalire lo stack. Tutto ciò porta al fatto che, per le chiamate che possono generare eccezioni, il compilatore aggiunge istruzioni supplementari. Quindi, se effettivamente non viene sollevata alcuna eccezione, il programma eseguirà comunque azioni non necessarie. Per ridurre i costi, LLVM ha alcune euristiche per definire situazioni in cui non è necessario aggiungere codice di gestione delle eccezioni o dove è possibile ridurre il numero di istruzioni 'superflue'.

Il relatore esamina circa una dozzina di queste e dimostra in quali situazioni esse aiutano ad accelerare l'esecuzione del programma e dove questi metodi non sono applicabili.

Così, Roman Rusyaev ha portato gli ascoltatori alla conclusione che il codice che gestisce le eccezioni non può sempre essere eseguito senza costi aggiuntivi, e offre i seguenti suggerimenti:

  • nella progettazione di librerie, è meglio rinunciare completamente alle eccezioni;
  • se le eccezioni sono necessarie, si dovrebbero aggiungere ovunque modificatori noexcept (e const), affinché il compilatore possa ottimizzare il più possibile.

In generale, il relatore ha confermato l'opinione secondo cui le eccezioni dovrebbero essere utilizzate il meno possibile o addirittura eliminate.

Le diapositive della relazione sono disponibili al seguente link: [«Eccezioni C++ attraverso la lente delle ottimizzazioni del compilatore LLVM»]

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

C++ Russia: com'è andata
Slide da una presentazione

Una delle numerose relazioni di questa conferenza, dedicata alle novità di C++20, si è distinta non solo per la presentazione colorata, ma anche per l'indicazione chiara dei problemi esistenti nella logica di elaborazione delle collezioni (ciclo for, callback).

Adi Shavit evidenzia i seguenti problemi: i metodi attuali percorrono l'intera collezione e non forniscono accesso a qualche stato intermedio interno (o lo fanno nel caso dei callback, ma con molti effetti collaterali indesiderabili, come il Callback Hell). Sembra esserci l'uso di iteratori, ma anche in questo caso le cose non sono così semplici: non ci sono punti d'ingresso e di uscita comuni (begin → end contro rbegin → rend e così via), e non è chiaro quanto tempo ci vorrà per iterare! A partire da C++20, questi problemi vengono affrontati!

La prima opzione: ranges. Grazie a un involucro sopra gli iteratori, otteniamo un'interfaccia comune per l'inizio e la fine dell'iterazione, oltre alla possibilità di composizione. Tutto ciò consente di costruire facilmente pipeline complete per l'elaborazione dei dati. Ma non è tutto così semplice: parte della logica dei calcoli si trova all'interno dell'implementazione di un particolare iteratore, il che può complicare la lettura e il debugging del codice.

C++ Russia: com'è andata
Slide da una presentazione

Beh, per questo caso in C++20 sono state aggiunte le coroutines (funzioni, il cui comportamento è simile ai generatori nel linguaggio Python): l'esecuzione può essere ritardata restituendo un valore attuale mantenendo lo stato intermedio. In questo modo, raggiungiamo non solo l'elaborazione dei dati man mano che vengono generati, ma anche incapsuliamo tutta la logica all'interno di una particolare coroutine.

Ma c'è un rovescio della medaglia: attualmente sono supportati solo parzialmente dai compilatori disponibili e la loro implementazione non è così accurata come si vorrebbe: ad esempio, per ora non è consigliabile utilizzare riferimenti e oggetti temporanei nelle coroutine. Inoltre, ci sono alcune limitazioni su ciò che può essere una coroutine, e le funzioni constexpr, i costruttori/distruttori, così come il main non fanno parte di questo elenco.

Pertanto, le coroutine risolvono una parte significativa dei problemi legati alla semplicità della logica di elaborazione dei dati, ma le loro implementazioni attuali richiedono ulteriori perfezionamenti.

Materiali:

Trucchi C++ da Yandex.Taxi, Anton Polukhin

Nella mia attività professionale a volte devo implementare semplici soluzioni ausiliarie: un'interfaccia tra l'interfaccia interna e le API di una libreria, registrazione o parsing. Di solito, in questo caso, non è necessaria alcuna ottimizzazione aggiuntiva. Ma cosa succede se questi componenti sono utilizzati in alcuni dei più popolari servizi di Runet? In questa situazione, dovremo gestire terabyte di log all'ora! Ogni millisecondo conta, quindi è necessario ricorrere a vari trucchi — di questo ha parlato Anton Polukhin.

Probabilmente, l'esempio più interessante è stata l'implementazione del pattern 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_; 
};

In questo esempio, inizialmente si desidera eliminare i file header delle librerie esterne: in questo modo la compilazione sarà più veloce e ci si proteggerà da possibili conflitti di nomi e altri errori simili. 

Bene, abbiamo spostato #include nel file .cpp: è necessaria una forward-declaration dell'API avvolta, così come std::unique_ptr. Ora abbiamo allocazioni dinamiche e altre cose sgradevoli come dati sparsi in heap e garanzie ridotte. A tutto ciò può aiutare 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 data_; 
};

L'unico problema: è necessario specificare la dimensione e l'allineamento per ogni wrapper — rendiamo il nostro pimpl un template con i parametri , utilizziamo valori arbitrarie e aggiungiamo nel distruttore un controllo per verificare che abbiamo indovinato tutto: 

~FastPimpl() noexcept { 
    validate(); 
    Ptr()->~T(); 
}

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

Poiché durante l'elaborazione del distruttore T è già definito, questo codice sarà corretto e darà errori di compilazione sui valori di dimensione e allineamento necessari. Così, al costo di una sola esecuzione aggiuntiva della compilazione, ci liberiamo dall'allocazione dinamica delle classi avvolte, nascondiamo le API nel file .cpp con l'implementazione e otteniamo una struttura più adatta alla cache della CPU.

La registrazione e il parsing sono apparsi meno impressionanti e pertanto non saranno menzionati in questa rassegna.

Le diapositive della relazione sono disponibili al seguente link: [«C++ трюки из Такси»]

Tecniche moderne per mantenere il codice DRY, Björn Fahller

In questa presentazione, Björn Fahller mostra diversi modi per affrontare una carenza stilistica come le verifiche di condizioni ripetute:

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

Ti suona familiare? Utilizzando alcune tecniche potenti di C++, emerse nei recenti standard, è possibile implementare elegantemente la stessa funzionalità senza alcuna perdita di prestazioni. Confronta:   

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

Per gestire un numero variabile di controlli, è naturale utilizzare template variadici ed espressioni fold. Supponiamo di voler verificare l'uguaglianza di più variabili a un elemento dell'enumerazione state_type. La prima cosa che viene in mente è scrivere una funzione ausiliaria is_any_of:


enum state_type { IDLE, CONNECTED, DISCONNECTED };

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

Questo risultato intermedio è deludente. Per ora il codice non diventa più leggibile:

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

Qualche aggiustamento della situazione può essere effettuato grazie ai parametri template non di tipo. Con il loro aiuto trasferiremo gli elementi enumerati dell'enumerazione in un elenco di parametri di template: 

template 
bool is_any_of(state_type t) { 
    return ((t == states) | ...); 
}
	
assert(is_any_of(state)); 

Utilizzando auto come parametro template non di tipo (C++17), l'approccio si estende semplicemente a confronti non solo con elementi di state_type, ma anche con tipi primitivi che possono essere utilizzati come parametri template non di tipo:


template 
bool is_any_of(const T& t) {
    return ((t == alternatives) | ...);
}

Attraverso tali miglioramenti successivi, si raggiunge la sintassi fluida desiderata per i controlli:


template 
struct any_of : private std::tuple {  
// prima di tutto, ereditiamo i costruttori da tuple  
        using std::tuple::tuple;
        template 
        bool operator ==(const T& t) const {
                return std::apply(
                        [&t](const auto& ... ts) {
                                return ((ts == t) || ...);
                        },
                        static_cast<const std::tuple&>(*this));
        }
};

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

In questo esempio, la guida di deduzione serve per suggerire al compilatore i parametri template desiderati per la struttura, conoscendo i tipi degli argomenti del costruttore. 

In seguito, diventa più interessante. Bjorn insegna a generalizzare il codice ottenuto per operatori di confronto oltre a ==, e poi per operazioni arbitrarie. Nel frattempo, con l'esempio d'uso vengono spiegate caratteristiche come l'attributo no_unique_address (C++20) e i parametri template nelle funzioni lambda (C++20). (Sì, ora la sintassi delle lambda è ancora più facile da ricordare – sono quattro coppie consecutive di parentesi di ogni tipo.) La soluzione finale che utilizza funzioni come componenti del costruttore mi scalda particolarmente il cuore, per non parlare dell'espressione tuple nelle migliori tradizioni del calcolo lambda.

Alla fine non dimentichiamo di dare una rifinitura:

  • Ricordiamo che le lambda sono constexpr senza costi aggiuntivi; 
  • Aggiungiamo il perfect forwarding e osserviamo la sua sintassi poco elegante applicata al parameter pack nelle chiusure delle lambda;
  • Diamo al compilatore maggiori opportunità di ottimizzazione con conditional noexcept; 
  • Ci preoccupiamo di fornire un'uscita degli errori più chiara nei template grazie ai valori di ritorno espliciti delle lambda. Questo costringerebbe il compilatore a effettuare più controlli prima della vera chiamata alla funzione template – nella fase di controllo dei tipi. 

Per ulteriori dettagli, rimandiamo al materiale della lezione: 

Le nostre impressioni

La nostra prima partecipazione a C++ Russia è stata memorabile per la sua ricchezza. Si ha l'impressione che C++ Russia sia un evento caloroso, dove il confine tra apprendimento e interazione sociale è quasi impercettibile. Tutto, dall'atteggiamento dei relatori ai concorsi dei partner dell'evento, invita a discuterne vivacemente. La parte sostanziale della conferenza, composta da relazioni, copre un ampio spettro di argomenti, comprese le novità di C++, esempi pratici di grandi progetti e considerazioni architettoniche ideologiche. Ma sarebbe ingiusto non prestare attenzione anche al lato sociale dell'evento, che contribuisce a superare le barriere linguistiche in relazione non solo a C++.

Ringraziamo gli organizzatori della conferenza per l'opportunità di partecipare a un simile evento!
Il post degli organizzatori sui passati, presenti e futuri eventi di C++ Russia potete trovarlo nel blog di JUG Ru.

Grazie per aver letto, e speriamo che il nostro resoconto degli eventi sia stato utile!

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster