要約
この投稿全体を読もうとする前に、次の点に注意してください。
- 提示された問題に対する解決策自分で見つけたしかし、私はまだその分析が正しいかどうかを知りたいと思っています。
fameta::counter
私は、残りのいくつかの問題を解決するクラスにソリューションをパッケージ化しました。githubで見つけてください;- で見ることができますゴッドボルトに取り組む。
すべてはこうして始まった
フィリップ・ロゼーンが2015年に黒魔術を発見/発明して以来、フレンドインジェクションによるコンパイル時カウンタはC++にあります、私はこのデバイスに少し夢中になっているので、CWG機能性を犠牲にしなければならないと決心した私はがっかりしましたが、説得力のある使用例をいくつか示すことで彼らの考えが変わるかもしれないとまだ期待していました。
それから、数年前にもう一度このことを調べてみることにしたのです。ユーバースイッチesネストできる - 興味深い使用例だと思う - しかし、それはもう機能しないだろう利用可能なコンパイラの新しいバージョンでは、2118号(そしてまだ) が開いている状態の場合: コードはコンパイルされますが、カウンターは増加しません。
問題は報告されていますロザンのウェブサイトそして最近はstackoverflowでも:C++ はコンパイル時のカウンターをサポートしていますか?
数日前、私は再び問題に取り組もうと決心した。
私は、まだ有効に思える C++ が動作しなくなった原因となったコンパイラーの変更点を理解したかったのです。そのために、インターネットでこの件について話している人がいないかと広く遠くまで検索しましたが、見つかりませんでした。そこで、実験を始めていくつかの結論に至りました。ここで、私よりも知識のある方からのフィードバックを期待して、その結論を発表します。
以下に、わかりやすくするために、ロゼーンの元のコードを示します。動作の説明については、彼のウェブサイトを参照してください:
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");
}
g++ と clang++ の最近のコンパイラでは、next()
常に 1 を返します。少し実験したところ、少なくとも g++ の問題は、関数が最初に呼び出されたときにコンパイラが関数テンプレートのデフォルト パラメータを評価すると、それ以降の関数の呼び出しではデフォルト パラメータの再評価がトリガーされず、新しい関数がインスタンス化されず、常に以前にインスタンス化された関数が参照されるということのようです。
最初の質問
- あなたは本当に私のこの診断に同意しますか?
- もしそうなら、この新しい動作は標準によって義務付けられていますか? 以前のものはバグでしたか?
- そうでない場合、何が問題なのでしょうか?
上記を念頭に置いて、私は回避策を思いつきました。next()
呼び出しごとに単調に増加する一意の ID を付けてマークし、呼び出し先に渡すことで、同じ呼び出しがなくなるため、コンパイラは毎回すべての引数を再評価することになります。
そうするのは面倒に思えますが、考えてみると、関数のようなマクロに隠された標準__LINE__
または__COUNTER__
類似のマクロ(使用可能な場合)を使用するだけで済みます。counter_next()
そこで私は、後ほど説明する問題を示す最も簡略化された形で次のことを思いつきました。
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>();
上記の結果は、ゴッドボルト面倒な人のためにスクリーンショットを撮っておきました。
ご覧の通り、7.0.0 までの trunk g++ および clang++ では動作します。カウンターは予想通り0から3に増加しますが、clang++バージョン7.0.0以上ではそうではありません。
さらに悪いことに、私は実際に、単に「コンテキスト」パラメータを追加して、カウンタが実際にそのコンテキストにバインドされ、新しいコンテキストが定義されるたびに再起動できるようにすることで、clang++ バージョン 7.0.0 までをクラッシュさせることに成功しました。これにより、カウンタを無限に使用できる可能性が開かれます。このバリアントでは、clang++ バージョン 7.0.0 以上はクラッシュしませんが、期待どおりの結果は得られません。ゴッドボルトでライブ。
何が起こっているのか全くわからなかった私は、翻訳元テンプレートがどのように、いつインスタンス化されるかを確認できるウェブサイトがあります。サービス私が思うに、clang++がではないfriend constexpr auto counter(slot<N>)
実際にインスタンス化されるたびに、関数のいずれかを定義しますwriter<N, I>
。
counter(slot<N>)
すでにインスタンス化されているはずの任意の N を明示的に呼び出そうとすると、この仮説の根拠となるようです。
ただし、すでにインスタンス化されているはずの任意の を明示的にインスタンス化しようとすると、writer<N, I>
clang ++ は再定義された についてエラーを出力します。N
I
friend constexpr auto counter(slot<N>)
上記をテストするために、前のソース コードにさらに 2 行追加しました。
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
すべて自分で見ることができますゴッドボルトについて下のスクリーンショット。
つまり、clang++ は、定義されていないと信じているものを定義したと信じている、ちょっと頭が混乱してしまいますよね?
2回目の質問
- 私の回避策まったく合法的な C++ なのか、それとも別の g++ のバグを発見してしまったのか?
- それが合法であるなら、私はいくつかの厄介な clang++ のバグを発見したことになるのでしょうか?
- それとも、私は未定義の動作の暗い地下世界に足を踏み入れてしまったので、責められるのは私だけなのでしょうか?
いずれにせよ、このウサギの穴から抜け出すのを手伝ってくれる人、必要であれば頭の痛い説明をしてくれる人を温かく歓迎します。:D
ベストアンサー1
さらに調査を進めたところ、関数に実行できる小さな変更が存在することが判明しましたnext()
。これにより、コードは 7.0.0 以上の clang++ バージョンでは正常に動作しますが、他のすべての clang++ バージョンでは動作しなくなります。
以前のソリューションから抜粋した次のコードをご覧ください。
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
それに気づけば、文字通りに関連付けられた値を読み取りslot<N>
、それに1を加算し、この新しい値を全く同じ slot<N>
。
slot<N>
に関連付けられた値がない場合、slot<Y>
代わりに に関連付けられた値が取得されます。この値は、に関連付けられた値を持つY
より小さい最大のインデックスになります。N
slot<Y>
上記のコードの問題は、g++では動作するものの、clang++では(当然のことながら)reader(0, slot<N>())
永久に関連付けられた値がない場合に返されたものを返しますslot<N>
。つまり、すべてのスロットが基本値に実際に関連付けられることになります0
。
解決策は、上記のコードを次のように変換することです。
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
return R;
}
slot<N>()
が に変更されていることに注意してくださいslot<N-1>()
。これは理にかなっています。 に値を関連付ける場合slot<N>
、まだ値が関連付けられていないことを意味し、したがって、それを取得しようとしても意味がありません。また、カウンタを増やしたいので、 に関連付けられたカウンタの値は に関連付けられslot<N>
た値に 1 を加えた値である必要がありますslot<N-1>
。
ユーレカ!
ただし、これにより clang++ バージョン <= 7.0.0 が壊れます。
結論
私が投稿した元の解決策には、次のような概念的なバグがあるように思われます。
- g++ には癖/バグ/緩和があり、それが私のソリューションのバグと相殺されて、結局はコードが動作するようになります。
- clang++ バージョン 7.0.0 以降はより厳格で、元のコード内のバグは許容されません。
- clang++ バージョン <= 7.0.0 には、修正されたソリューションが機能しないバグがあります。
まとめると、次のコードは g++ と 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
コードはそのままでも msvc で動作します。icc コンパイラは を使用する場合に SFINAE をトリガーせず、 が原因decltype(counter(slot<N>()))
でできないというエラーを出力します。deduce the return type of function "counter(slot<N>)"
it has not been defined
これはバグだと思うは、 の直接の結果に対して SFINAE を実行することで回避できますcounter(slot<N>)
。 これは他のすべてのコンパイラでも機能しますが、g++ は大量の非常に迷惑な警告を吐き出し、それをオフにすることはできません。 したがって、この場合も、 が#ifdef
助けになる可能性があります。
の証拠はゴッドボルトにある、スクリーンショットは下記にあります。