C++ Rusia: bagaimana hal itu terjadi

Jika di awal permainan Anda mengatakan bahwa ada kode C++ yang tergantung di dinding, maka pada akhirnya kode itu pasti akan menyerang Anda.

Bjarne Stroustrup

Dari tanggal 31 Oktober hingga 1 November, konferensi C++ Russia Piter diadakan di St. Petersburg - salah satu konferensi pemrograman berskala besar di Rusia, yang diselenggarakan oleh JUG Ru Group. Pembicara yang diundang meliputi anggota Komite Standar C++, pembicara CppCon, penulis buku O'Reilly, dan pengelola proyek seperti LLVM, libc++, dan Boost. Konferensi ini ditujukan untuk pengembang C++ berpengalaman yang ingin memperdalam keahlian mereka dan bertukar pengalaman dalam komunikasi langsung. Pelajar, mahasiswa pascasarjana dan guru universitas diberikan diskon yang sangat bagus.

Konferensi edisi Moskow akan tersedia untuk dikunjungi pada awal April tahun depan, namun sementara itu siswa kami akan memberi tahu Anda hal-hal menarik apa yang mereka pelajari pada acara terakhir. 

C++ Rusia: bagaimana hal itu terjadi

Foto dari album konferensi

Tentang kami

Dua mahasiswa dari Sekolah Tinggi Ekonomi Universitas Riset Nasional - St. Petersburg mengerjakan postingan ini:

  • Liza Vasilenko adalah mahasiswa sarjana tahun ke-4 yang mempelajari Bahasa Pemrograman sebagai bagian dari program Matematika Terapan dan Ilmu Komputer. Setelah mengenal bahasa C++ pada tahun pertama saya di universitas, saya kemudian memperoleh pengalaman bekerja dengannya melalui magang di industri. Kecintaan saya terhadap bahasa pemrograman pada umumnya dan pemrograman fungsional pada khususnya mempengaruhi pemilihan laporan di konferensi tersebut.
  • Danya Smirnov adalah mahasiswa tahun pertama program master β€œPemrograman dan Analisis Data”. Saat masih di sekolah, saya menulis soal olimpiade dalam bahasa C++, dan kemudian kebetulan bahasa tersebut terus-menerus muncul dalam kegiatan pendidikan dan akhirnya menjadi bahasa kerja utama. Saya memutuskan untuk berpartisipasi dalam konferensi untuk meningkatkan pengetahuan saya dan juga belajar tentang peluang baru.

Dalam buletin tersebut, pimpinan fakultas sering berbagi informasi tentang acara-acara pendidikan yang berkaitan dengan spesialisasi kami. Pada bulan September kami melihat informasi tentang C++ Russia dan memutuskan untuk mendaftar sebagai pendengar. Ini adalah pengalaman pertama kami berpartisipasi dalam konferensi semacam itu.

Struktur konferensi

  • Laporan

Selama dua hari, para ahli membaca 30 laporan, yang mencakup banyak topik hangat: penggunaan fitur bahasa yang cerdik untuk memecahkan masalah yang diterapkan, pembaruan bahasa yang akan datang sehubungan dengan standar baru, kompromi dalam desain C++ dan tindakan pencegahan saat menangani konsekuensinya, contoh arsitektur proyek yang menarik, serta beberapa detail infrastruktur bahasa. Tiga pertunjukan berlangsung secara bersamaan, paling sering dua dalam bahasa Rusia dan satu dalam bahasa Inggris.

  • Zona diskusi

Usai pidato, semua pertanyaan yang belum diajukan dan diskusi yang belum selesai dipindahkan ke area khusus untuk komunikasi dengan pembicara, dilengkapi dengan papan penanda. Cara yang baik untuk mengisi jeda antar pidato dengan percakapan yang menyenangkan.

  • Lightning Talks dan diskusi informal

Jika Anda ingin memberikan laporan singkat, Anda dapat mendaftar di papan tulis untuk Lightning Talk malam hari dan mendapatkan waktu lima menit untuk membicarakan apa pun tentang topik konferensi. Misalnya, pengenalan singkat tentang sanitizer untuk C++ (bagi sebagian orang ini merupakan hal baru) atau cerita tentang bug pada pembangkitan gelombang sinus yang hanya dapat didengar, namun tidak terlihat.

Format lainnya adalah diskusi panel β€œKomite Dari Hati ke Hati.” Di atas panggung ada beberapa anggota komite standardisasi, di proyektor ada perapian (secara resmi - untuk menciptakan suasana yang tulus, tetapi alasan β€œkarena SEMUANYA TERBAKAR” tampak lebih lucu), pertanyaan tentang standar dan visi umum C++ , tanpa diskusi teknis dan holiwar yang memanas. Ternyata panitia juga berisi orang-orang hidup yang mungkin belum sepenuhnya yakin akan suatu hal atau mungkin belum mengetahui sesuatu.

Bagi penggemar holivar, acara ketiga tetap ada - sesi BOF β€œGo vs. C++”. Kami mengajak seorang pecinta Go, seorang pecinta C++, sebelum memulai sesi mereka bersama-sama menyiapkan 100500 slide tentang suatu topik (seperti masalah dengan paket di C++ atau kurangnya generik di Go), dan kemudian mereka berdiskusi dengan hidup di antara mereka sendiri dan dengan penonton, dan penonton mencoba memahami dua sudut pandang sekaligus. Jika holivar dimulai di luar konteks, moderator akan turun tangan dan mendamaikan para pihak. Format ini membuat ketagihan: beberapa jam setelah dimulainya, hanya setengah dari slide yang diselesaikan. Akhir zaman harus dipercepat secara signifikan.

  • Mitra berdiri

Mitra konferensi diwakili di aula - di stan mereka berbicara tentang proyek terkini, menawarkan magang dan pekerjaan, mengadakan kuis dan kompetisi kecil, dan juga mengundi hadiah-hadiah menarik. Pada saat yang sama, beberapa perusahaan bahkan menawarkan untuk melalui wawancara tahap awal, yang dapat bermanfaat bagi mereka yang datang tidak hanya untuk mendengarkan laporan.

Rincian teknis laporan

Kami mendengarkan laporan selama dua hari. Kadang-kadang sulit untuk memilih satu laporan dari laporan paralel - kami sepakat untuk berpisah dan bertukar pengetahuan yang diperoleh selama istirahat. Meski begitu, tampaknya masih banyak yang tertinggal. Di sini kami ingin membicarakan isi beberapa laporan yang menurut kami paling menarik

Pengecualian dalam C++ melalui prisma optimasi kompiler, Roman Rusyaev

C++ Rusia: bagaimana hal itu terjadi
Geser dari presentasi

Seperti judulnya, Roman melihat bekerja dengan pengecualian menggunakan LLVM sebagai contoh. Pada saat yang sama, bagi mereka yang tidak menggunakan Clang dalam pekerjaannya, laporan tersebut masih dapat memberikan gambaran tentang bagaimana kode tersebut berpotensi dioptimalkan. Hal ini terjadi karena pengembang kompiler dan perpustakaan standar terkait berkomunikasi satu sama lain dan banyak solusi yang berhasil dapat terjadi secara bersamaan.

Jadi, untuk menangani pengecualian, Anda perlu melakukan banyak hal: memanggil kode penanganan (jika ada) atau sumber daya gratis pada level saat ini dan menaikkan tumpukan lebih tinggi. Semua ini mengarah pada fakta bahwa kompiler menambahkan instruksi tambahan untuk panggilan yang berpotensi menimbulkan pengecualian. Oleh karena itu, jika pengecualian tidak benar-benar dimunculkan, program akan tetap melakukan tindakan yang tidak perlu. Untuk mengurangi overhead, LLVM memiliki beberapa heuristik untuk menentukan situasi di mana kode penanganan pengecualian tidak perlu ditambahkan atau jumlah instruksi β€œekstra” dapat dikurangi.

Pembicara membahas selusin di antaranya dan menunjukkan situasi di mana metode tersebut membantu mempercepat pelaksanaan program, dan situasi di mana metode ini tidak dapat diterapkan.

Jadi, Roman Rusyaev mengarahkan siswa pada kesimpulan bahwa kode yang berisi penanganan pengecualian tidak selalu dapat dieksekusi dengan overhead nol, dan memberikan saran berikut:

  • ketika mengembangkan perpustakaan, pada prinsipnya ada baiknya mengabaikan pengecualian;
  • jika pengecualian masih diperlukan, maka jika memungkinkan, ada baiknya menambahkan pengubah noException (dan const) di mana pun sehingga kompiler dapat mengoptimalkan sebanyak mungkin.

Secara umum, pembicara menegaskan pandangan bahwa pengecualian sebaiknya digunakan seminimal mungkin atau ditinggalkan sama sekali.

Slide laporan dapat dilihat pada link berikut: ["Pengecualian C++ melalui lensa optimasi kompiler LLVM"]

Generator, coroutine, dan hal-hal manis lainnya yang menggugah pikiran, Adi Shavit

C++ Rusia: bagaimana hal itu terjadi
Geser dari presentasi

Salah satu dari banyak laporan di konferensi ini yang didedikasikan untuk inovasi dalam C++20 berkesan tidak hanya karena presentasinya yang penuh warna, namun juga karena identifikasi yang jelas mengenai masalah yang ada dengan logika pemrosesan kumpulan (untuk loop, callback).

Adi Shavit menyoroti hal berikut: metode yang tersedia saat ini melewati seluruh koleksi dan tidak memberikan akses ke beberapa keadaan perantara internal (atau mereka melakukannya dalam kasus panggilan balik, tetapi dengan sejumlah besar efek samping yang tidak menyenangkan, seperti Callback Hell) . Tampaknya ada iterator, tetapi bahkan dengan mereka semuanya tidak begitu lancar: tidak ada titik masuk dan keluar yang sama (mulai β†’ akhir versus rbegin β†’ rend dan seterusnya), tidak jelas berapa lama kita akan mengulanginya? Dimulai dengan C++20, masalah ini terpecahkan!

Opsi pertama: rentang. Dengan menggabungkan iterator, kami mendapatkan antarmuka umum untuk awal dan akhir iterasi, dan kami juga mendapatkan kemampuan untuk menulis. Semua ini memudahkan pembuatan jalur pemrosesan data yang lengkap. Namun tidak semuanya berjalan mulus: bagian dari logika perhitungan terletak di dalam implementasi iterator tertentu, yang dapat mempersulit pemahaman dan debug kode.

C++ Rusia: bagaimana hal itu terjadi
Geser dari presentasi

Nah, untuk kasus ini, C++20 menambahkan coroutine (fungsi yang perilakunya mirip dengan generator di Python): eksekusi dapat ditunda dengan mengembalikan beberapa nilai saat ini sambil mempertahankan keadaan perantara. Dengan demikian, kami mencapai tidak hanya bekerja dengan data seperti yang terlihat, tetapi juga merangkum semua logika di dalam coroutine tertentu.

Namun ada kekurangannya: saat ini mereka hanya didukung sebagian oleh kompiler yang ada, dan juga tidak diimplementasikan dengan rapi seperti yang kita inginkan: misalnya, masih belum ada gunanya menggunakan referensi dan objek sementara di coroutine. Selain itu, ada beberapa batasan mengenai apa yang bisa menjadi coroutine, dan fungsi constexpr, konstruktor/destruktor, dan main tidak disertakan dalam daftar ini.

Oleh karena itu, coroutine memecahkan sebagian besar masalah dengan kesederhanaan logika pemrosesan data, namun implementasinya saat ini memerlukan perbaikan.

Bahan:

Trik C++ dari Yandex.Taxi, Anton Polukhin

Dalam aktivitas profesional saya, terkadang saya harus menerapkan hal-hal tambahan semata: pembungkus antara antarmuka internal dan API beberapa perpustakaan, logging atau parsing. Dalam hal ini, biasanya tidak diperlukan optimasi tambahan apa pun. Tetapi bagaimana jika komponen ini digunakan di salah satu layanan paling populer di Runet? Dalam situasi seperti ini, Anda harus memproses log berukuran terabyte per jam saja! Kemudian setiap milidetik berarti dan oleh karena itu Anda harus menggunakan berbagai trik - Anton Polukhin membicarakannya.

Mungkin contoh yang paling menarik adalah penerapan pola 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_; 
};

Dalam contoh ini, pertama-tama saya ingin menghapus file header perpustakaan eksternal - ini akan dikompilasi lebih cepat, dan Anda dapat melindungi diri Anda dari kemungkinan konflik nama dan kesalahan serupa lainnya. 

Oke, kita memindahkan #include ke file .cpp: kita memerlukan deklarasi penerusan API yang dibungkus, serta std::unique_ptr. Sekarang kita memiliki alokasi dinamis dan hal-hal tidak menyenangkan lainnya seperti data yang tersebar di banyak data dan jaminan yang berkurang. std::aligned_storage dapat membantu dengan semua ini. 

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

Satu-satunya masalah: Anda perlu menentukan ukuran dan perataan untuk setiap pembungkus - mari buat template pimpl kami dengan parameter , gunakan beberapa nilai arbitrer dan tambahkan tanda centang ke destruktor bahwa semuanya benar: 

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

Karena T sudah ditentukan saat memproses destruktor, kode ini akan diurai dengan benar dan pada tahap kompilasi akan menampilkan ukuran yang diperlukan dan nilai penyelarasan yang perlu dimasukkan sebagai kesalahan. Jadi, dengan biaya satu kali kompilasi tambahan, kami menghilangkan alokasi dinamis dari kelas yang dibungkus, menyembunyikan API dalam file .cpp dengan implementasinya, dan juga mendapatkan desain yang lebih cocok untuk di-cache oleh prosesor.

Logging dan parsing sepertinya kurang mengesankan dan oleh karena itu tidak akan disebutkan dalam ulasan ini.

Slide laporan dapat dilihat pada link berikut: ["Trik C++ dari Taksi"]

Teknik modern untuk menjaga kode Anda KERING, BjΓΆrn Fahller

Dalam pembicaraan ini, BjΓΆrn Fahller menunjukkan beberapa cara berbeda untuk mengatasi kelemahan gaya pemeriksaan kondisi berulang:

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

Kedengarannya familier? Dengan menggunakan beberapa teknik C++ canggih yang diperkenalkan dalam standar terbaru, Anda dapat mengimplementasikan fungsi yang sama secara elegan tanpa penalti performa apa pun. Membandingkan:   

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

Untuk menangani jumlah pemeriksaan yang tidak tetap, Anda harus segera menggunakan templat variadik dan ekspresi lipat. Mari kita asumsikan bahwa kita ingin memeriksa kesetaraan beberapa variabel dengan elemen state_type enum. Hal pertama yang terlintas dalam pikiran adalah menulis fungsi pembantu 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) || ...); 
}

Hasil antara ini mengecewakan. Sejauh ini kode tersebut tidak menjadi lebih mudah dibaca:

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

Parameter templat non-tipe akan sedikit membantu memperbaiki situasi. Dengan bantuan mereka, kami akan mentransfer elemen enumerable dari enum ke daftar parameter templat: 

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

Dengan menggunakan auto dalam parameter templat non-tipe (C++17), pendekatan ini secara sederhana menggeneralisasikan perbandingan tidak hanya dengan elemen state_type, namun juga dengan tipe primitif yang dapat digunakan sebagai parameter templat non-tipe:


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

Melalui perbaikan berturut-turut ini, sintaksis pemeriksaan lancar yang diinginkan tercapai:


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

Dalam contoh ini, panduan deduksi berfungsi untuk menyarankan parameter templat struktur yang diinginkan kepada kompiler, yang mengetahui tipe argumen konstruktor. 

Selanjutnya - lebih menarik. Bjorn mengajarkan cara menggeneralisasi kode yang dihasilkan untuk operator perbandingan di luar ==, dan kemudian untuk operasi arbitrer. Sepanjang prosesnya, fitur seperti atribut no_unique_address (C++20) dan parameter templat dalam fungsi lambda (C++20) dijelaskan menggunakan contoh penggunaan. (Ya, sekarang sintaksis lambda lebih mudah diingat - ini adalah empat pasang tanda kurung yang berurutan.) Solusi akhir yang menggunakan fungsi sebagai detail konstruktor benar-benar menghangatkan jiwa saya, belum lagi ekspresi tupel dalam tradisi lambda terbaik kalkulus.

Pada akhirnya, jangan lupa untuk memolesnya:

  • Ingatlah bahwa lambda adalah constexpr gratis; 
  • Mari tambahkan penerusan sempurna dan lihat sintaks jeleknya sehubungan dengan paket parameter di penutupan lambda;
  • Mari kita beri kompiler lebih banyak peluang untuk optimasi dengan kondisional nokecuali; 
  • Mari kita jaga keluaran kesalahan yang lebih mudah dipahami dalam templat berkat nilai pengembalian lambda yang eksplisit. Ini akan memaksa kompiler untuk melakukan lebih banyak pemeriksaan sebelum fungsi template benar-benar dipanggil - pada tahap pemeriksaan tipe. 

Untuk lebih jelasnya silakan merujuk pada materi perkuliahan: 

Kesan kami

Partisipasi pertama kami di C++ Russia sangat mengesankan karena intensitasnya. Saya mendapat kesan bahwa C++ Russia adalah acara yang tulus, di mana batas antara pelatihan dan komunikasi langsung hampir tidak terlihat. Semuanya, mulai dari mood pembicara hingga kompetisi dari mitra acara, kondusif untuk diskusi yang memanas. Isi konferensi, yang terdiri dari laporan, mencakup topik yang cukup luas termasuk inovasi C++, studi kasus proyek besar, dan pertimbangan arsitektur ideologis. Namun tidak adil jika mengabaikan komponen sosial dari acara tersebut, yang membantu mengatasi hambatan bahasa tidak hanya terkait dengan C++.

Kami berterima kasih kepada penyelenggara konferensi atas kesempatan untuk berpartisipasi dalam acara semacam ini!
Anda mungkin pernah melihat postingan penyelenggara tentang masa lalu, masa kini, dan masa depan C++ Rusia di blog JUG Ru.

Terima kasih telah membaca, dan kami harap penceritaan kembali peristiwa kami bermanfaat!

Sumber: www.habr.com

Tambah komentar