C ++ mengkompilasi penghitung waktu, ditinjau kembali


28

TL; DR

Sebelum Anda mencoba membaca seluruh posting ini, ketahuilah bahwa:

  1. solusi untuk masalah yang disajikan telah ditemukan sendiri , tetapi saya masih ingin tahu apakah analisisnya benar;
  2. Saya telah mengemas solusi ke dalam fameta::counterkelas yang memecahkan beberapa kebiasaan yang tersisa. Anda dapat menemukannya di github ;
  3. 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

  1. Apakah Anda benar-benar setuju dengan diagnosis saya ini?
  2. Jika ya, apakah perilaku baru ini diamanatkan oleh standar? Apakah yang sebelumnya bug?
  3. 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.

masukkan deskripsi gambar di sini

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 Ndan Iyang 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.

dentang ++ percaya itu telah mendefinisikan sesuatu yang ia yakini belum didefinisikan

Jadi, kelihatannya dentang ++ percaya itu telah mendefinisikan sesuatu yang ia yakini belum didefinisikan , jenis apa yang membuat kepala Anda berputar, bukan?


Batch kedua pertanyaan

  1. Apakah solusi saya legal C ++ sama sekali, atau apakah saya berhasil menemukan bug g ++ lainnya?
  2. Jika ini legal, apakah saya menemukan beberapa bug ++ clang yang tidak menyenangkan?
  3. 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



2
Seingat saya komite standar, orang memiliki niat yang jelas untuk melarang konstruksi waktu kompilasi dalam bentuk, bentuk atau bentuk apa pun yang tidak menghasilkan hasil yang persis sama setiap kali mereka (secara hipotesis) dievaluasi. Jadi itu mungkin bug kompiler, mungkin kasus "tidak terbentuk, tidak diperlukan diagnosa" atau mungkin sesuatu yang terlewatkan oleh standar. Namun demikian itu bertentangan dengan "semangat standar". Saya menyesal. Saya akan menyukai kompilasi penghitung waktu juga.
bolov

@HolyBlackCat Saya harus mengakui bahwa saya merasa sangat sulit untuk mendapatkan kepala saya di sekitar kode itu. Memang terlihat seperti itu bisa menghindari kebutuhan untuk secara eksplisit melewati angka yang meningkat secara monoton sebagai parameter untuk 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/566849
Fabio A.

@FabioA. Saya juga tidak sepenuhnya mengerti jawaban itu. Sejak mengajukan pertanyaan itu, saya menyadari bahwa saya tidak ingin menyentuh counter constexpr lagi.
HolyBlackCat

Walaupun ini adalah eksperimen pemikiran kecil yang menyenangkan, seseorang yang benar-benar menggunakan kode itu akan sangat berharap bahwa itu tidak akan berfungsi dalam versi C ++ di masa depan, kan? Dalam arti itu, hasilnya mendefinisikan dirinya sebagai bug.
Aziuth

Jawaban:


5

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.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

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 sama slot<N> .

Ketika slot<N>tidak memiliki nilai terkait, nilai yang dikaitkan dengan slot<Y>diambil sebagai gantinya, dengan Ymenjadi indeks tertinggi kurang dari Nyang slot<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 ketika slot<N>tidak memiliki nilai terkait. Pada gilirannya, ini berarti bahwa semua slot terkait secara efektif dengan nilai dasar 0.

Solusinya adalah mengubah kode di atas menjadi yang ini:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

Perhatikan bahwa slot<N>()telah diubah menjadi slot<N-1>(). Masuk akal: jika saya ingin mengaitkan suatu nilai slot<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 terkait slot<N>harus bernilai tambah slot<N-1>.

Eureka!

Ini memecah dentang versi ++ <= 7.0.0, meskipun.

Kesimpulan

Menurut saya solusi asli yang saya posting memiliki bug konseptual, sehingga:

  • g ++ memiliki quirk / bug / relaksasi yang dibatalkan dengan bug solusi saya dan akhirnya membuat kode tetap bekerja.
  • dentang versi ++> 7.0.0 lebih ketat dan tidak suka bug dalam kode asli.
  • dentang versi ++ <= 7.0.0 memiliki bug yang membuat solusi yang diperbaiki tidak berfungsi.

Menjumlahkan semua itu, kode berikut berfungsi pada semua versi g ++ dan clang ++.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

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 bisa deduce the return type of function "counter(slot<N>)"karena it has not been defined. Saya percaya ini adalah bug , yang dapat diselesaikan dengan melakukan SFINAE pada hasil langsung dari counter(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, #ifdefbisa datang menyelamatkan.

The pembuktian ada pada godbolt , screnshotted bawah.

masukkan deskripsi gambar di sini


2
Saya pikir respons semacam ini menutup topik, tetapi saya masih ingin tahu apakah saya benar dalam analisis saya, oleh karena itu saya akan menunggu sebelum menerima jawaban saya sendiri sebagai benar, berharap orang lain akan lewat dan memberi saya petunjuk yang lebih baik atau konfirmasi. :)
Fabio A.
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.