Constオーバーロードがgccで予期せず呼び出されました。コンパイラのバグまたは互換性の修正?


8

charおよびconst char配列のテンプレートのオーバーロードに依存する、はるかに大きなアプリケーションがあります。gcc 7.5、clang、およびビジュアルスタジオでは、以下のコードはすべてのケースで「NON-CONST」を出力します。ただし、gcc 8.1以降の場合、出力は次のようになります。

#include <iostream>

class MyClass
{
public:
    template <size_t N>
    MyClass(const char (&value)[N])
    {
        std::cout << "CONST " << value << '\n';
    }

    template <size_t N>
    MyClass(char (&value)[N])
    {
        std::cout << "NON-CONST " << value << '\n';
    }
};

MyClass test_1()
{
    char buf[30] = "test_1";
    return buf;
}

MyClass test_2()
{
    char buf[30] = "test_2";
    return {buf};
}

void test_3()
{
    char buf[30] = "test_3";
    MyClass x{buf};
}

void test_4()
{
    char buf[30] = "test_4";
    MyClass x(buf);
}

void test_5()
{
    char buf[30] = "test_5";
    MyClass x = buf;
}

int main()
{
    test_1();
    test_2();
    test_3();
    test_4();
    test_5();
}

(godboltからの)gcc 8および9の出力は次のとおりです。

CONST test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5

これはコンパイラのバグのようですが、言語の変更に関連する他の問題である可能性があります。誰かが決定的に知っていますか?


異なるC ++標準バージョンでコンパイルしてみましたか?
n314159

1
g ++とclang ++は異なります:godbolt.org/z/g3cCBL
Ted Lyngmo

@ n314159いい質問、私はただやった。-std = c ++ 11 -std = c ++ 14 -std = c ++ 17および-std = c ++ 2aは、すべて同じ「悪い」結果を生成します。-std = c ++ 03でコンパイルしないでください。
Rob L

1
@TedLyngmoはい、ビジュアルスタジオと同様に、clangは期待どおりに機能することに注意しました。
Rob L

回答:


6

関数(関数のローカルオブジェクトを指定したもの)から単純なid式を返す場合、コンパイラーはオーバーロードの解決を2回実行する必要があります。最初に、それはそれが右辺値ではなく、右辺値であるかのように扱われます。最初のオーバーロードの解決が失敗した場合にのみ、オブジェクトを左辺値として再度実行されます。

[class.copy.elision]

3次のコピー初期化コンテキストでは、コピー操作の代わりに移動操作が使用される場合があります。

  • returnステートメントの式が、最も内側の囲み関数またはラムダ式の本体またはparameter-declaration-clauseで宣言された自動ストレージ期間を持つオブジェクトを指定する(おそらく括弧で囲まれた)id式である場合、または

  • ...

コピーのコンストラクタを選択するためのオーバーロード解決は、オブジェクトが右辺値で指定されているかのように最初に実行されます。最初のオーバーロード解決が失敗したか実行されなかった場合、または選択されたコンストラクターの最初のパラメーターのタイプがオブジェクトのタイプへの右辺値参照ではない場合(cv修飾されている可能性があります)、オブジェクトを左辺値。[注:この2段階のオーバーロードの解決は、コピーの省略が発生するかどうかに関係なく実行する必要があります。これは、省略が実行されない場合に呼び出されるコンストラクターを決定し、選択されたコンストラクターは、呼び出しが省略された場合でもアクセス可能でなければなりません。—エンドノート]

右辺値オーバーロードを追加する場合、

template <size_t N>
MyClass (char (&&value)[N])
{
    std::cout << "RVALUE " << value << '\n';
}

出力は

RVALUE test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5

そしてこれは正しいでしょう。正しくないのは、ご覧のGCCの動作です。最初のオーバーロードの解決が成功したと見なします。これは、const左辺値参照が右辺値にバインドする可能性があるためです。ただし、「または、選択したコンストラクターの最初のパラメーターの型がオブジェクトの型への右辺値参照でない場合」というテキストは無視されます。それによると、最初のオーバーロード解決の結果を破棄し、再度実行する必要があります。

まあ、それはとにかくC ++ 17までの状況です。現在の標準草案は何か違うことを言っています。

最初のオーバーロード解決が失敗または実行されなかった場合、式またはオペランドを左辺値と見なして、オーバーロード解決が再度実行されます。

C ++ 17までのテキストは削除されました。だから、それは時間旅行のバグです。GCCはC ++ 20の動作を実装しますが、標準がC ++ 17の場合でも実装します。


ありがとうございました!また、右辺値のオーバーロードを追加して、この特定のケースで機能する回避策も提供されているようです。それをchar&versionと同じにするだけです。
Rob L

1
@RobL-喜んでお手伝いします。もともと思っていたより少し微妙な状況になっていますのでご注意ください。テキストは確かに変わりました。チェックしてよかったです。
StoryTeller-Unslander Monica

これは、clang++C ++ 20の実装も、元のコードですべての場合にNON-CONSTバージョンを使用しているため、正しくないことを意味すると思います。
Ted Lyngmo

2
@TedLyngmo-時間旅行の問題では、それは本当にいつ問題になるかです。Clangの開発者がこの変更を実装するためにうまくいかなかったと思います。それ自体をバグとは呼びません。C ++ 17で新しくGCCを実行することはおそらくバグです。この変更が標準にどのように入力されたかによって異なります。これを遡及的に変更する必要のある欠陥報告があったとは思わないので、GCCのバグだと思います。複数の標準のサポートは微妙な作業です。
StoryTeller-Unslander Monica

1
@RobL-これは関数ローカルオブジェクトです。returnステートメントの後、それはなくなっています。これは意図的な最適化ポイントです。コピーする代わりに、オブジェクトを共食いにすることができます。標準的な方法は、渡される値のカテゴリに関係なく正しく動作する型を記述することです。
StoryTeller-Unslander Monica

0

これが「直感的な振る舞い」であるかどうかはコメントで議論されているので、この振る舞いの背後にある理由を試してみようと思いました。

CPPCONで行われたかなり良い講演があります。これにより、私には少しわかりやすくなります{ 講演スライド }。基本的に、非const参照を取る関数は何を意味しますか?入力オブジェクトが読み取り/書き込み可能である必要があること。さらに強力なのは、このオブジェクトを変更するつもりであることを意味し、この関数には副作用があります。const refは読み取り専用を意味し、rvalue refはリソースを取得できることを意味します。場合test_1()呼び出す終わるしたNON-CONSTコンストラクタを、それが意味するだろう、私は終わりだ後、それはもはや存在しないにも関わらず、このオブジェクトを変更しようとしますこれはバグだと思います(初期化中に参照がどのようにバインドされるかは、渡された引数がconstであるかどうかに依存します)。

私にとってもう少し気になるのは、によって導入された繊細さtest_2()です。ここでは、上記の[class.copy.elision]に関するルールの代わりに、コピーリストの初期化が行われています。これで、MyClassタイプのオブジェクトを、で初期化したかのように返すので、ビヘイビアーが呼び出されます。私は常にinit-listsをより簡潔にする方法として考えてきましたが、ここでは中括弧は意味的に大きな違いをもたらします。のコンストラクタが多数の引数をとる場合、これはさらに重要になります。次に、を作成し、それを変更して、多数の引数を指定してそれを返し、ビヘイビアーを呼び出したいとします。たとえば、コンストラクタがあるとします。bufNON-CONSTMyClassbufCONST

template <size_t N>
MyClass(const char (&value)[N], int)
{
    std::cout << "CONST int " << value << '\n';
}

template <size_t N>
MyClass(char (&value)[N], int)
{
    std::cout << "NON-CONST int " << value << '\n';
}

そしてテスト:

MyClass test_0() {
    char buf[30] = "test_0";
    return {buf,0};
}

ゴッドボルトNON-CONSTCONSTおそらく私たちが望んでいることですが(関数と引数のセマンティクスのクールエイドを飲んだ後)、動作を取得することを通知します。しかし今、コピーリストの初期化は私たちが望むことをしません。次のテストの種類は私のポイントをより良くします:

MyClass test_0() {
    char buf[30] = "test_0";
    buf[0] = 'T';
    const char (&bufR)[30]{buf};
    return {bufR,0};
}
// OUTPUT: CONST int Test_0

コピーリストの初期化で適切なセマンティクスを取得するには、最後にバッファを「リバウンド」する必要があります。このオブジェクトが他のMyClassオブジェクトを初期化することが目的だったと思いますがNON-CONST、適切な動作が何であれmove / copy-constructorが呼び出されれば、return copy-listの動作を使用するだけで問題ありませんが、それはかなり聞こえ始めています繊細。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.