TL; DR
Sebelum Anda mencoba membaca seluruh posting ini, ketahuilah bahwa:
- solusi untuk masalah yang disajikan telah ditemukan sendiri , tetapi saya masih ingin tahu apakah analisisnya benar;
- Saya telah mengemas solusi ke dalam
fameta::counter
kelas yang memecahkan beberapa kebiasaan yang tersisa. Anda dapat menemukannya di github ; - Anda bisa melihatnya bekerja di godbolt .
Bagaimana semuanya dimulai
Sejak Filip Roséen menemukan / menemukan, pada 2015, sihir hitam yang menyusun penghitung waktu ada di C ++ , saya agak terobsesi dengan perangkat, jadi ketika CWG memutuskan bahwa fungsionalitas harus berjalan, saya kecewa, tetapi masih berharap bahwa pikiran mereka dapat diubah dengan menunjukkan kepada mereka beberapa kasus penggunaan yang menarik.
Kemudian, beberapa tahun yang lalu saya memutuskan untuk memiliki melihat hal itu lagi, sehingga uberswitch es bisa bersarang - penggunaan kasus yang menarik, menurut pendapat saya - hanya untuk menemukan bahwa itu tidak akan bekerja lagi dengan versi baru kompiler yang tersedia, meskipun masalah 2118 adalah (dan masih ) dalam keadaan terbuka: kode akan dikompilasi, tetapi penghitung tidak akan meningkat.
Masalahnya telah dilaporkan di situs web Roséen dan baru-baru ini juga di stackoverflow: Apakah C ++ mendukung penghitung waktu kompilasi?
Beberapa hari yang lalu saya memutuskan untuk mencoba dan mengatasi masalah lagi
Saya ingin memahami apa yang telah berubah dalam kompiler yang membuat, tampaknya masih valid C ++, tidak berfungsi lagi. Untuk itu, saya telah mencari di mana-mana untuk menemukan seseorang yang telah membicarakannya, tetapi tidak berhasil. Jadi saya sudah mulai bereksperimen dan sampai pada beberapa kesimpulan, yang saya presentasikan di sini berharap untuk mendapatkan umpan balik dari yang lebih tahu daripada di sekitar sini.
Di bawah ini saya menyajikan kode asli Rosen demi kejelasan. Untuk penjelasan tentang cara kerjanya, silakan merujuk ke situs webnya :
template<int N>
struct flag {
friend constexpr int adl_flag (flag<N>);
};
template<int N>
struct writer {
friend constexpr int adl_flag (flag<N>) {
return N;
}
static constexpr int value = N;
};
template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
return N;
}
template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
return R;
}
int constexpr reader (float, flag<0>) {
return 0;
}
template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
return R;
}
int main () {
constexpr int a = next ();
constexpr int b = next ();
constexpr int c = next ();
static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}
Dengan g ++ dan clang ++ compiler new-ish, next()
selalu mengembalikan 1. Setelah bereksperimen sedikit, masalah setidaknya dengan g ++ tampaknya bahwa begitu kompiler mengevaluasi fungsi templat parameter default saat pertama fungsi dipanggil, setiap panggilan berikutnya untuk fungsi-fungsi itu tidak memicu evaluasi ulang dari parameter default, sehingga tidak pernah membuat instance fungsi baru tetapi selalu mengacu pada yang sebelumnya dipakai.
Pertanyaan pertama
- Apakah Anda benar-benar setuju dengan diagnosis saya ini?
- Jika ya, apakah perilaku baru ini diamanatkan oleh standar? Apakah yang sebelumnya bug?
- Jika tidak, lalu apa masalahnya?
Dengan mengingat hal-hal di atas, saya datang dengan mengerjakan: menandai setiap next()
invokasi dengan id unik yang meningkat secara monoton, untuk meneruskan ke betis, sehingga tidak ada panggilan yang sama, sehingga memaksa kompiler untuk mengevaluasi kembali semua argumen tiap kali.
Tampaknya menjadi beban untuk melakukan itu, tetapi memikirkannya orang hanya bisa menggunakan makro standar __LINE__
atau __COUNTER__
-seperti (jika tersedia), tersembunyi di counter_next()
makro fungsi-seperti.
Jadi saya datang dengan yang berikut ini, yang saya sajikan dalam bentuk paling sederhana yang menunjukkan masalah yang akan saya bicarakan nanti.
template <int N>
struct slot;
template <int N>
struct slot {
friend constexpr auto counter(slot<N>);
};
template <>
struct slot<0> {
friend constexpr auto counter(slot<0>) {
return 0;
}
};
template <int N, int I>
struct writer {
friend constexpr auto counter(slot<N>) {
return I;
}
static constexpr int value = I-1;
};
template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
return R;
};
template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
return R;
};
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();
Anda dapat mengamati hasil di atas pada godbolt , yang telah saya saring untuk para pemalas.
Dan seperti yang Anda lihat, dengan trunk g ++ dan clang ++ hingga 7.0.0 berfungsi! , penghitung meningkat dari 0 menjadi 3 seperti yang diharapkan, tetapi dengan clang ++ versi di atas 7.0.0 tidak .
Untuk menambah penghinaan pada cedera, saya sebenarnya berhasil membuat dentang ++ hingga versi 7.0.0 crash, dengan hanya menambahkan parameter "konteks" ke dalam campuran, sehingga penghitung benar-benar terikat pada konteks itu dan, dengan demikian, dapat dimulai kembali kapan saja konteks baru didefinisikan, yang membuka kemungkinan untuk menggunakan penghitung yang berpotensi tak terbatas. Dengan varian ini, dentang ++ di atas versi 7.0.0 tidak crash, tetapi masih tidak menghasilkan hasil yang diharapkan. Tinggal di godbolt .
Karena kehilangan petunjuk tentang apa yang sedang terjadi, saya telah menemukan situs web cppinsights.io , yang memungkinkan orang melihat bagaimana dan kapan template akan dipakai. Menggunakan layanan itu yang saya pikir sedang terjadi adalah bahwa dentang ++ sebenarnya tidak mendefinisikan friend constexpr auto counter(slot<N>)
fungsi apa pun setiap kali writer<N, I>
instantiated.
Mencoba untuk secara eksplisit meminta counter(slot<N>)
N yang diberikan yang seharusnya sudah dipakai tampaknya memberikan dasar untuk hipotesis ini.
Namun, jika saya mencoba secara eksplisit instantiate writer<N, I>
untuk setiap yang diberikan N
dan I
yang seharusnya sudah instantiated, maka dentang ++ mengeluh tentang redefined friend constexpr auto counter(slot<N>)
.
Untuk menguji di atas, saya telah menambahkan dua baris lagi ke kode sumber sebelumnya.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Anda dapat melihat semuanya sendiri di godbolt . Tangkapan layar di bawah.
Jadi, kelihatannya dentang ++ percaya itu telah mendefinisikan sesuatu yang ia yakini belum didefinisikan , jenis apa yang membuat kepala Anda berputar, bukan?
Batch kedua pertanyaan
- Apakah solusi saya legal C ++ sama sekali, atau apakah saya berhasil menemukan bug g ++ lainnya?
- Jika ini legal, apakah saya menemukan beberapa bug ++ clang yang tidak menyenangkan?
- Atau apakah aku baru saja memasuki dunia gelap Perilaku yang Tidak Terdefinisi, jadi aku sendiri yang harus disalahkan?
Dalam hal apa pun, saya akan dengan hangat menyambut siapa pun yang ingin membantu saya keluar dari lubang kelinci ini, mengeluarkan penjelasan tentang kepala jika perlu. : D
next()
fungsi, namun saya tidak bisa benar-benar mencari tahu bagaimana cara kerjanya. Dalam hal apa pun, saya telah membuat respons terhadap masalah saya sendiri, di sini: stackoverflow.com/a/60096865/566849Jawaban:
Setelah penyelidikan lebih lanjut, ternyata ada modifikasi kecil yang dapat dilakukan untuk
next()
fungsi, yang membuat kode berfungsi dengan baik pada versi dentang ++ di atas 7.0.0, tetapi membuatnya berhenti bekerja untuk semua versi dentang ++ lainnya.Lihat kode berikut, diambil dari solusi saya sebelumnya.
Jika Anda memperhatikannya, apa yang benar - benar dilakukannya adalah mencoba membaca nilai yang terkait dengannya
slot<N>
, tambahkan 1 padanya dan kemudian kaitkan nilai baru ini dengan yang samaslot<N>
.Ketika
slot<N>
tidak memiliki nilai terkait, nilai yang dikaitkan denganslot<Y>
diambil sebagai gantinya, denganY
menjadi indeks tertinggi kurang dariN
yangslot<Y>
memiliki nilai terkait.Masalah dengan kode di atas adalah bahwa, meskipun bekerja pada g ++, clang ++ (memang seharusnya, saya katakan?) Membuat
reader(0, slot<N>())
secara permanen mengembalikan apa pun yang dikembalikan ketikaslot<N>
tidak memiliki nilai terkait. Pada gilirannya, ini berarti bahwa semua slot terkait secara efektif dengan nilai dasar0
.Solusinya adalah mengubah kode di atas menjadi yang ini:
Perhatikan bahwa
slot<N>()
telah diubah menjadislot<N-1>()
. Masuk akal: jika saya ingin mengaitkan suatu nilaislot<N>
, itu berarti belum ada nilai yang dikaitkan, oleh karena itu tidak masuk akal untuk mencoba mengambilnya. Kami juga ingin meningkatkan penghitung, dan nilai penghitung yang terkaitslot<N>
harus bernilai tambahslot<N-1>
.Eureka!
Ini memecah dentang versi ++ <= 7.0.0, meskipun.
Kesimpulan
Menurut saya solusi asli yang saya posting memiliki bug konseptual, sehingga:
Menjumlahkan semua itu, kode berikut berfungsi pada semua versi g ++ dan clang ++.
Kode as-is juga berfungsi dengan msvc. The ICC compiler tidak memicu SFINAE saat menggunakan
decltype(counter(slot<N>()))
, lebih memilih untuk mengeluh tentang tidak bisadeduce the return type of function "counter(slot<N>)"
karenait has not been defined
. Saya percaya ini adalah bug , yang dapat diselesaikan dengan melakukan SFINAE pada hasil langsung daricounter(slot<N>)
. Ini bekerja pada semua kompiler lain juga, tetapi g ++ memutuskan untuk meludahkan sejumlah besar peringatan yang sangat menjengkelkan yang tidak dapat dimatikan. Jadi, juga dalam hal ini,#ifdef
bisa datang menyelamatkan.The pembuktian ada pada godbolt , screnshotted bawah.
sumber