C++ の std::vector と参照の組み合わせには注意

f:id:shogonir:20170914230407p:plain

 

目次

  1. この記事の目的
  2. 問題のコード
  3. 理想と現実
  4. 何が起こったのか
  5. 解決策
  6. まとめ

 

1. この記事の目的

私がC++に慣れてきたと思った矢先にどハマりした問題を共有します。
C++のstd::vectorを、ただの可変長配列とかリストだと理解している方は注意です。
私はまさに「JavaArrayListみたいなやつ」と理解して使っていました。
では、どういったコードを書くと問題が起きるのか書いていきます。

 

2. 問題のコード

問題のコードは以下のようにstd::vectorの要素の参照を保持し、vectorに追加や削除する実装です。

 

#include <vector>
#include <iostream>

class Point
{
public:
    Point(int x, int y): x(x), y(y) {}
public:
    int x;
    int y;
};

int main()
{
    std::vector<Point> points;
    points.push_back(Point(1, 1));
    
    Point& head = points[0];
    std::cout << "head(" << head.x << ", " << head.y << ")" << std::endl;
    
    std::cout << "points push back" << std::endl;
    points.push_back(Point(1, 1));
    
    std::cout << "head(" << head.x << ", " << head.y << ")" << std::endl;
    
    return 0;
}

 

3. 理想と現実

このコードでどのように動作してほしかったかを書いていきます。
理想は最初に保持した参照が変化しないで下記のように出力されることです。

 

head(1, 1)
points push back
head(1, 1)

 

しかし実際にWandboxで動かしてみた結果は下記の通りです。

 

head(1, 1)
points push back
head(0, 0)

 

std::vectorに要素を追加すると、なぜか最初に保持した参照の中が変化しています。
さて、なぜこのようなことになったのか説明します。

 

4. 何が起こったのか

上述のような動作になったのは、vectorに要素を追加した際に実態の場所が変わったためです。

vectorはそもそも、データの実態を配列として保持しています。
しかし可変長配列として振る舞う必要があるため、必要な要素数よりも少し長い配列領域を確保しています。
現在の要素数はsize()で取得できますが、配列領域の最大数はcapacity()で取得できます。

このキャパシティを超えるような要素追加が行われると、別の場所に配列領域を確保します。
その様子を以下のように画像にまとめてみました。

 

f:id:shogonir:20170915000002p:plain

 

この、「要素を追加した際に配列領域を確保しなおすかもしれない」ことに注意です。
問題のコードでは、最初に取得した参照が、キャパシティを超えた要素追加によって無効になったためでした。

 

5. 解決策

解決策は単純で、vectorの要素を参照ではなく実態で保持することです。
コードにすると以下のようになります。 アンパサンド(&)を削除するだけです。

 

#include <vector>
#include <iostream>

class Point
{
public:
    Point(int x, int y): x(x), y(y) {}
public:
    int x;
    int y;
};

int main()
{
    std::vector<Point> points;
    points.push_back(Point(1, 1));
    
    Point head = points[0];
    std::cout << "head(" << head.x << ", " << head.y << ")" << std::endl;
    
    std::cout << "points push back" << std::endl;
    points.push_back(Point(1, 1));
    
    std::cout << "head(" << head.x << ", " << head.y << ")" << std::endl;
    
    return 0;
}

 

6. まとめ

std::vectorに要素を追加する可能性がある場所では、std::vectorの要素を参照で保持しないようにしましょう。