C ++コンパイル時間カウンター、再訪


28

TL; DR

この投稿全体を読む前に、次のことを知っておいてください。

  1. 提示された問題の解決策は自分見つけましたが、分析が正しいかどうかを知りたいと思っています。
  2. fameta::counter残りのいくつかの癖を解決するクラスにソリューションをパッケージ化しました。githubで見つけることができます
  3. godboltの作業でそれを見ることができます。

すべての始まり

FilipRoséenが発見/発明して以来、2015年に、時間カウンターコンパイルするブラックマジックはC ++にあるため、デバイスにややこだわっていたため、CWG が機能を実行する必要があると判断したときはがっかりしましたが、彼らの心はいくつかの説得力のある使用例を示すことで変更できます。

次に、数年前、私はそのことをもう一度確認することにしました。これにより、uberswitch esを入れ子にできるようになりました-私の意見では興味深いユースケースです- の新しいバージョンではもはや機能ないことを発見しただけです。問題2118がオープン状態であっても(現在も)利用可能なコンパイラー:コードはコンパイルされますが、カウンターは増加しません。

この問題は、RoséenのWebサイト、および最近ではstackoverflowでも報告さています。C++はコンパイル時カウンターをサポートしていますか?

数日前、もう一度問題に取り組み、取り組むことを決めました

コンパイラーの何が変わったのかを理解したかったのです。そのために、私は誰かがそれについて話してくれるようにインターウェブをずっと遠くまで検索しましたが、役に立ちませんでした。だから私は実験を始め、いくつかの結論に達しました。私はここで提示しているのですが、ここでは自分よりも知識のある人からフィードバックを得たいと思っています。

以下では、わかりやすくするために、Roséenの元のコードを示します。それがどのように機能するかの説明については、彼のウェブサイト参照してください

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 ++の問題は、コンパイラが関数テンプレートのデフォルトパラメータを初めて評価すると、関数が初めて呼び出された後、これらの関数はデフォルトのパラメーターの再評価をトリガーしないため、新しい関数をインスタンス化することはなく、常に以前にインスタンス化された関数を参照します。


最初の質問

  1. この私の診断に実際に同意しますか?
  2. はいの場合、この新しい動作は規格によって義務付けられていますか?以前のものはバグでしたか?
  3. そうでない場合、問題は何ですか?

上記を念頭に置いて、私はnext()回避策を思い付きました:呼び出しが同じではないように、呼び出しをすべて単調に増加する一意のIDでマークして、呼び出し先に渡して、コンパイラーにすべての引数を再評価させる毎回。

それを行うのは負担のように見えますが、それを考えると、関数のようなマクロに隠された、標準の__LINE__または__COUNTER__-like(使用可能な場合はどこでも)マクロを使用できます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>();

上記の結果は、私がレイジーのためにスクリーンショットを撮ったgodboltで見ることができます。

ここに画像の説明を入力してください

ご覧のとおり、トランクg ++とclang ++では7.0.0まで機能します。、期待どおりにカウンターが0から3に増加しますが、7.0.0より上のバージョンのclang ++では増加しません

傷害に侮辱を加えるために、実際に「コンテキスト」パラメーターをミックスに追加するだけで、バージョン7.0.0までのclang ++をクラッシュさせ、カウンターが実際にそのコンテキストにバインドされるようにしました。新しいコンテキストが定義されるたびに再起動され、無限の可能性があるカウンターを使用する可能性が開かれます。このバリアントを使用すると、バージョン7.0.0より上のclang ++はクラッシュしませんが、期待した結果が得られません。ゴッドボルトに住んでいます。

何が起こっているのかについての手がかりを失って、私はcppinsights.io Webサイトを発見しました。これにより、テンプレートがインスタンス化される方法と時期を確認できます。そのサービスを使用して私が起こっていると思うことは、clang ++ がインスタンス化されるたびに実際に関数を定義しないことです。friend constexpr auto counter(slot<N>)writer<N, I>

counter(slot<N>)すでにインスタンス化されているはずの特定のN を明示的に呼び出そうとすることが、この仮説の基礎を与えるようです。

私は明示的にインスタンス化しようとする場合は、writer<N, I>任意の与えられたためNIそれはすでにインスタンス化されている必要があり、その後、打ち鳴らす++が文句を再定義しましたfriend constexpr auto counter(slot<N>)

上記をテストするために、前のソースコードにさらに2行追加しました。

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;

あなたはそれをすべてゴッドボルトで見ることができます。下のスクリーンショット。

clang ++は、それが定義していないと信じている何かを定義したと信じています

それで、clang ++は、それが定義していないと信じている何かを定義したと信じているようです、どの種類のあなたの頭をスピンさせますか?


質問の2番目のバッチ

  1. 私の回避策は合法的なC ++ですか、それとも別のg ++​​バグを発見できましたか?
  2. それが合法である場合、私はいくつかの厄介なclang ++バグを発見しましたか?
  3. それとも、未定義の動作の暗い地下世界を掘り下げただけなのか、私自身が唯一のせいですか?

いずれにせよ、このウサギの穴から抜け出すのを手伝ってくれたなら誰でも歓迎します。:D



2
標準委員会の人々は覚えているように、(仮説的に)評価されるたびに正確に同じ結果を生成しない、あらゆる種類、形状、形式のコンパイル時の構成を禁止する明確な意図があります。したがって、それはコンパイラのバグかもしれませんし、「形式が正しくなく、診断が必要ない」ケースかもしれませんし、標準が見落としているかもしれません。それにもかかわらず、それは「標準の精神」に反します。ごめんなさい。コンパイル時間カウンターも好きだったでしょう。
bolov

@HolyBlackCat私は、そのコードに頭を悩ませるのが非常に難しいことを認めなければなりません。単調に増加する数をパラメーターとしてnext()関数に明示的に渡す必要性を回避できるように見えますが、それがどのように機能するのか本当にわかりません。いずれにしても、私は自分の問題に対する応答を思いつきました

@FabioA。私もその答えを完全には理解していません。その質問をして以来、私はconstexprカウンターに二度と触れたくないことに気づきました。
HolyBlackCat

これは楽しいちょっとした実験ですが、実際にそのコードを使用した人は、C ++の将来のバージョンでは機能しないことを予想する必要があるでしょう。その意味で、結果はバグとして定義されます。
Aziuth

回答:


5

さらなる調査の結果、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されます。関連付けられた値Nslot<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 ++にはquirk / bug / relaxationがあり、私のソリューションのバグでキャンセルされ、最終的にはコードが機能します。
  • 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、救助に来ることができます。

証明はgodbolt上にある以下のscrenshotted、。

ここに画像の説明を入力してください


2
この回答はトピックを締めくくるものだと思いますが、私が自分の分析が正しいかどうかを知りたいので、自分の答えを正解として受け入れる前に、他の誰かが通り過ぎてより良い手がかりを得るのを待っていますまたは確認。:)
ファビオA.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.