std :: vectorに追加するときのクラスフィールドの奇妙な動作


31

次の状況で、(clangおよびGCCで)非常に奇妙な動作が見つかりました。nodesクラスのインスタンスである1つの要素を持つベクトルがありますNode。次に、ベクターにnodes[0]新しい関数を追加する関数を呼び出しますNode。新しいノードが追加されると、呼び出し元オブジェクトのフィールドがリセットされます!ただし、関数が終了すると、通常の状態に戻ります。

これは最小限の再現可能な例だと思います:

#include <iostream>
#include <vector>

using namespace std;

struct Node;
vector<Node> nodes;

struct Node{
    int X;
    void set(){
        X = 3;
        cout << "Before, X = " << X << endl;
        nodes.push_back(Node());
        cout << "After, X = " << X << endl;
    }
};

int main() {
    nodes = vector<Node>();
    nodes.push_back(Node());

    nodes[0].set();
    cout << "Finally, X = " << nodes[0].X << endl;
}

どの出力

Before, X = 3
After, X = 0
Finally, X = 3

Xがプロセスによって変更されないままであることを期待しますが。

私が試した他のこと:

  • Nodeinside を追加する行を削除すると、set()毎回X = 3が出力されます。
  • 新規に作成してNodeそれを呼び出すと(Node p = nodes[0])、出力は3、3、3になります。
  • 参照を作成Nodeしてその(Node &p = nodes[0])で呼び出すと、出力は3、0、0になります(おそらく、ベクトルのサイズが変更されると参照が失われるためです)。

これは何らかの理由で未定義の動作ですか?どうして?


4
en.cppreference.com/w/cpp/container/vector/push_backを参照してください。あなたが呼ばれているとしたらreserve(2)呼び出す前にベクトルにset()これを行動に定義されます。しかし、setそのような関数を書くことは、reserve未定義の動作を避けるために呼び出す前に適切な十分なサイズをユーザーに要求することは悪い設計なので、それを行わないでください。
JohnFilleau

回答:


39

コードの動作が未定義です。に

void set(){
    X = 3;
    cout << "Before, X = " << X << endl;
    nodes.push_back(Node());
    cout << "After, X = " << X << endl;
}

へのアクセスXは、実際にthis->Xありthis、ベクターのメンバーへのポインターです。あなたが行うときnodes.push_back(Node());、あなたはベクトルとそのプロセスの再割り当て、に新しい要素を追加し、すべてのイテレータ、ポインタと参照を無効にベクトルの要素に。つまり

cout << "After, X = " << X << endl;

this無効なを使用しています。


push_backすでに定義されていない動作を呼び出していますか(それから、無効化されたメンバー関数にいるためthis)、またはthisポインターを初めて使用するときにUBが発生しますか?つまり、それは可能でしょうreturn 42;か?
n314159

3
@ n314159 nodesNodeインスタンスから独立しているため、呼び出しにUBはありませんpush_back。その後、UBは無効なポインターを使用しています。
NathanOliver

@ n314159これを概念化する良い方法は、関数を想像するvoid set(Node* this)ことです。無効なポインタを渡すことや、関数内でそれに渡すことは未定義ではありませんfree()。確かではありませんが、((Node*) nullptr)->set()使用せずthis、メソッドが仮想でない場合でも定義されていると思います。
DutChen18

((Node *) nullptr)->set()これはnullポインターを逆参照するので、私はそれは大丈夫だとは思いません(と同等に書くと、コレがはっきりとわかります(*((Node *) nullptr)).set();)。
n314159

1
@Deduplicator表現を更新しました。
NathanOliver

15
nodes.push_back(Node());

ベクトルを再割り当てして、のアドレスを変更しますがnodes[0]this更新されません。メソッドを次のコードに
置き換えてみてくださいset

    void set(){
        X = 3;
        cout << "Before, X = " << X << endl;
        cout << "Before, this = " << this << endl;
        cout << "Before, &nodes[0] = " << &nodes[0] << endl;
        nodes.push_back(Node());
        cout << "After, X = " << X << endl;
        cout << "After, this = " << this << endl;
        cout << "After, &nodes[0] = " << &nodes[0] << endl;
    }

&nodes[0]呼び出しpush_backた後の違いに注意してください。

-fsanitize=addressこれをキャッチし、さらにでコンパイルすると、メモリが解放された行を通知します-g

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