Kami menempatkan garis dalam parameter template

Modern C ++ telah membawa kita banyak fitur yang sebelumnya kurang dalam bahasa. Untuk mendapatkan efek yang serupa, kruk yang menakjubkan ditemukan untuk waktu yang lama, terutama terdiri dari kain kaki dan pola makro yang sangat besar (seringkali juga autogenous). Tetapi sekarang, dari waktu ke waktu, kebutuhan muncul untuk peluang yang masih belum ada dalam bahasa. Dan kita mulai menemukan kembali desain kompleks dari template dan makro lagi, menghasilkannya dan mencapai perilaku yang kita butuhkan. Ini hanya cerita seperti itu.

Selama setengah tahun terakhir, saya dua kali membutuhkan nilai yang dapat digunakan dalam parameter template. Pada saat yang sama, saya ingin memiliki nama yang dapat dibaca manusia untuk nilai-nilai ini dan mengecualikan kebutuhan untuk menyatakan nama-nama ini sebelumnya. Tugas khusus yang saya selesaikan adalah masalah terpisah, mungkin nanti saya akan menulis posting terpisah tentang mereka, di suatu tempat di hub "pemrograman abnormal". Sekarang saya akan berbicara tentang pendekatan yang saya gunakan untuk menyelesaikan masalah ini.

Jadi, ketika datang ke parameter template, kita bisa menggunakan tipe atau nilai const statis. Untuk sebagian besar tugas, ini lebih dari cukup. Kami ingin menggunakan pengidentifikasi yang dapat dibaca manusia dalam parameter - kami mendeklarasikan struktur, penghitungan atau konstanta dan menggunakannya. Masalah dimulai ketika kami tidak dapat menentukan pengidentifikasi ini sebelumnya dan ingin melakukannya di tempat.

Adalah mungkin untuk mendeklarasikan struktur atau kelas secara langsung dalam parameter templat. Ini bahkan akan berfungsi jika templat tidak melakukan apa pun dengan parameter ini yang memerlukan uraian lengkap struktur. Selain itu, kami tidak dapat mengontrol namespace di mana struktur seperti itu dinyatakan. Dan penggantian template yang benar-benar identik akan berubah menjadi kode yang sama sekali berbeda jika baris ini berada di kelas atau ruang nama yang berdekatan.

Anda perlu menggunakan literal, dan semua literal di C ++, hanya literal karakter dan string literal yang bisa disebut dibaca. Tapi literal karakter dibatasi hingga empat karakter (saat menggunakan char32_t), dan string literal adalah array karakter dan nilainya tidak dapat diteruskan ke parameter template.

Ternyata semacam lingkaran setan. Anda harus mengumumkan sesuatu terlebih dahulu, atau menggunakan pengidentifikasi yang tidak nyaman. Mari kita coba untuk mendapatkan bahasa yang tidak diadaptasi. Bagaimana jika menerapkan makro yang akan membuat sesuatu yang cocok untuk digunakan dalam argumen templat dari string literal?

Mari kita membuat struktur untuk string


Pertama, mari kita buat dasar untuk string. Di C ++ 11, argumen templat variadic muncul.
Kami mendeklarasikan struktur yang berisi karakter string dalam argumen:

template <char... Chars> struct String{}; 

github

Itu bekerja. Kami bahkan dapat langsung menggunakan baris-baris ini seperti ini:

 template <class T> struct Foo {}; Foo<String<'B', 'a', 'r'>> foo; 

Sekarang seret baris ini ke dalam runtime


Bagus Tidak akan buruk untuk bisa mendapatkan nilai dari string ini dalam runtime. Biarkan ada struktur template tambahan yang akan mengekstraksi argumen dari string seperti itu dan membuat konstanta darinya:

 template <class T> struct Get; template <char... Chars> struct Get<String<Chars...>> { static constexpr char value[] = { Chars... }; }; 

Ini juga berfungsi. Karena baris kami tidak mengandung '\ 0' di akhir, Anda perlu hati-hati beroperasi dengan konstanta ini (lebih baik, menurut saya, untuk segera membuat string_view menggunakan konstanta dan sizeof dari itu dalam argumen konstruktor). Orang bisa menambahkan '\ 0' di akhir array, tetapi ini tidak perlu untuk tugas saya.

Periksa apakah kami dapat memanipulasi string semacam itu


Oke, apa lagi yang bisa Anda lakukan dengan string seperti itu? Misalnya gabungkan:

 template <class A, class B> struct Concatenate; template <char... Chars, char... ExtraChars...> struct Concatenate<String<Chars...>, String<ExtraChars...>> { using type = String<Chars..., ExtraChars...>; }; 

github

Pada prinsipnya, Anda dapat melakukan lebih atau kurang operasi apa pun (saya belum mencobanya, karena saya tidak membutuhkannya, tetapi saya hanya dapat membayangkan bagaimana Anda dapat mencari substring atau bahkan mengganti substring).
Sekarang kita memiliki pertanyaan utama, bagaimana cara mengekstraksi karakter dari string literal dalam waktu kompilasi dan menempatkannya dalam argumen templat.

Gambar burung hantu. Tulis makro.


Mari kita mulai dengan cara menempatkan karakter dalam argumen templat satu per satu:

 template <class T, char c> struct PushBackCharacter; template <char... Chars, char c> struct PushBackCharacter<String<Chars...>, c> { using type = String<Chars..., c>; }; template <char... Chars> struct PushBackCharacter<String<Chars...>, '\0'> { using type = String<Chars...>; }; 

github

Saya menggunakan spesialisasi terpisah untuk karakter '\ 0', agar tidak menambahkannya ke string yang digunakan. Selain itu, ini agak menyederhanakan bagian lain dari makro.

Berita baiknya adalah string literal dapat menjadi parameter untuk fungsi constexpr. Kami akan menulis fungsi yang akan mengembalikan karakter dengan indeks dalam string atau '\ 0' jika string lebih pendek dari indeks (di sini spesialisasi PushBackCharacter untuk karakter '\ 0' berguna).

 template <size_t N> constexpr char CharAt(const char (&s)[N], size_t i) { return i < N ? s[i] : '\0'; } 

Pada dasarnya, kita sudah bisa menulis sesuatu seperti ini:

 PushBackCharacter< PushBackCharacter< PushBackCharacter< PushBackCharacter< String<>, CharAt("foo", 0) >::type, CharAt("foo", 1) >::type, CharAt("foo", 2) >::type, CharAt("foo", 3) >::type 

Kami meletakkan tapak kaki seperti itu, tetapi lebih asli (kami dapat menulis skrip untuk menghasilkan kode) di dalam makro kami, dan hanya itu!

Ada nuansa. Jika jumlah karakter dalam garis lebih besar dari level bersarang di makro, garis hanya akan terpotong dan kami bahkan tidak akan menyadarinya. Kekacauan.

Mari kita membuat satu struktur lagi, yang tidak mengonversi string yang tiba padanya, tetapi static_assert sehingga panjangnya tidak melebihi konstanta:

 #define _NUMBER_TO_STR(n) #n #define NUMBER_TO_STR(n) _NUMBER_TO_STR(n) template <class String, size_t size> struct LiteralSizeLimiter { using type = String; static_assert(size <= MAX_META_STRING_LITERAL_SIZE, "at most " NUMBER_TO_STR(MAX_META_STRING_LITERAL_SIZE) " characters allowed for constexpr string literal"); }; #undef NUMBER_TO_STR #undef _NUMBER_TO_STR 

Makro, akan terlihat seperti ini:

 #define MAX_META_STRING_LITERAL_SIZE 256 #define STR(literal) \ ::LiteralSizeLimiter< \ ::PushBackCharacter< \ ... \ ::PushBackCharacter< \ ::String<> \ , ::CharAt(literal, 0)>::type \ ... \ , ::CharAt(literal, 255)>::type \ , sizeof(literal) - 1>::type 

github

Ternyata


 template <class S> std::string_view GetContent() { return std::string_view(Get<S>::value, sizeof(Get<S>::value)); } std::cout << GetContent<STR("Hello Habr!")>() << std::endl; 

Implementasi yang saya dapatkan dapat ditemukan di github .

Akan sangat menarik bagi saya untuk mendengar tentang kemungkinan aplikasi dari mekanisme ini, berbeda dari yang saya buat.

Source: https://habr.com/ru/post/id438146/


All Articles