25052


C++
でポリモーフィズムを利用した実装をしたいのですが、並列処理をしているわけでもないのに、プログラムを実行する度にプログラムが落ちたり落ちなかったりします。

以下のコードは、現在発生しているバグに関係していると思われる本質部分を抜き出したものです。

[code]

    #include <iostream>
    #include <vector>
    using namespace std;
    
    class AbstractNumber {
    public:
      virtual void show() = 0;
    protected:
      int n_;
    };
    
    class Odd : public AbstractNumber {
    public:
      Odd(int n) { n_ = n; }
      void show() override { cout << "Odd: " << n_ << endl; }
    };
    
    class Even : public AbstractNumber {
    public:
      Even(int n) { n_ = n; }
      void show() override { cout << "Even: " << n_ << endl; }
    };
    
    class Controller {
    public:
      void add(int n) {
        if (n % 2 == 0) {
          even_.push_back(Even(n));
          numbers_.push_back(&even_.back());
        } else {
          odd_.push_back(Odd(n));
          numbers_.push_back(&odd_.back());
        }
      }
      void show() {
        for (int i = 0; i < numbers_.size(); ++i) numbers_[i]->show();
      }
    private:
      vector<AbstractNumber *> numbers_;
      vector<Odd>  odd_;
      vector<Even> even_;
    };
    
    int main() {
      Controller c;
      for (int i = 0; i < 10; ++i) c.add(i);
      c.show();
      return 0;
    }
    
[/code]

`Controller` クラスは、`Odd` 型と `Even` 型それぞれの `vector`
配列、および、ポリモーフィズムを用いるための、`AbstractNumber *` 型の `vector` 配列をメンバとして持っています。

`Controller` クラスの `add()` メソッドで引数に整数を渡すと、引数の偶奇に応じて `Even` / `Odd`
クラスのオブジェクトが作成され、そのオブジェクトへのポインタが `numbers_` へプッシュされます。  
その状態で `Controller` クラスの `show()` メソッドが呼ばれると、`numbers_` が指している各オブジェクトの `show()`
メソッドがポリモーフィズムにより適切に呼び出され、標準出力に出力される……という動作を想定して書いたものです。

しかし、このプログラムを実際に実行してみると、 **成功したり（0 ～ 9 まで正しく表示される）、`show()`
で例外が発生してプログラムが落ちたりします**。  
また、 **プログラムが落ちる位置も毎回変わり** 、何も表示されずに落ちたり、半分ほど表示されてから落ちたりします。

以上を踏まえて質問なのですが、

  * なぜ、成功したり成功しなかったりするのでしょうか。
  * このバグの原因は何でしょうか。どうすれば意図通りに動作するようになるでしょうか。

以上、長くなりましたが、宜しくお願いいたします。  
ここまで目を通していただきありがとうございました。

* * *

なお、`Odd` 型オブジェクトや `Even` 型オブジェクトの実体を `Controller` クラスに持たせるのではなく、これらのオブジェクトを
`Controller` クラス外（たとえば `main()`）で作成し、そのポインタを `Controller`
クラスに持たせるような設計だと、このようなバグは発生しないことを確認しています。  
ですが、処理の都合上、`Odd` 型オブジェクトや `Even` 型オブジェクトの実体も `Controller`
クラスに持たせたいため、今回のような実装になっています。

使用環境は Wndows 10 64bit，Visual Studio 2015 Update 2 です。


CC BY-SA 3.0
2016-05-20T20:09:45.037
25051
2017-07-25T17:49:05.753
2016-05-20T20:38:51.483
14965
14965
question
c++ stl
ポリモーフィズム利用時に非決定的なバグが混入してしまう


原因は `vector` でしょう。

`vector`
は、内部で確保している配列のサイズが足りなくなると自動的に配列のサイズを増やします。大きなサイズの配列を新しく確保した後、古い配列の値を新しい配列にコピーあるいは
move してから、古い配列を解放します。

[code]

      even_.push_back(Even(n));
      numbers_.push_back(&even_.back());
    
[/code]

上のコードで `even_` のサイズが大きくなった時、`even_` の中の配列は新しくなるのに、既に `number_` に入っている `even_`
の要素のポインタは古い配列のままで更新されません。したがって、後で `show()` で `number_`
を参照したとき、古い配列に割り当てられていたメモリが上書きされていなければ、見た目上、うまくいきますし、`vtable`
のところが上書きされていれば落ちてしまいます。

解決するには、`even_`、 `odd_`、 `numbers_` は `shared_ptr<AbstractNumber>` を入れる
`vector` にし、そこに動的に生成した `Odd` や `Even` を入れていけばいいと思います。


CC BY-SA 3.0
2016-05-20T21:39:14.510
25052
2016-05-20T21:39:14.510
3605
25051
answer


ポリモーフィックな使い方の基底クラスを作りたい場合、基本クラスの AbstractNumber には通常はvirtualなデストラクタを書くべきです。  
numbers_ には Odd と Even の両方のインスタンスへのポインタがAbstractNumber
へのポインタとして格納されますが、AbstractNumber
のデストラクタがvirtualでない場合、解放操作が発生したとき、OddやEvenのデストラクタが呼ばれず未定義動作となってしまいます。よくあるエラー動作はOddやEvenで追加のメンバ変数を入れたとき、それが解放されずにメモリリークするなどですが、このプログラムでは特にそれはやっていないため動いたと思います。  
とはいえ、仕様ではこのような操作結果は未定義動作なので「たまたま動いただけ」とみなすべきです。  
また、メンバ変数はprivateにするのが一般的です。

（動的な）ポリモーフィックな使い方を想定していないクラスでは必ずしも基底クラスのデストラクタをvirtual
にはしません。たとえばSTLコンテナはそもそもpublic継承することを前提としていないのでデストラクタはvirtualではありません。これをpublic継承するのは危険があります。


CC BY-SA 3.0
2017-07-25T17:41:48.373
36673
2017-07-25T17:49:05.753
2017-07-25T17:49:05.753
24517
24517
25051
answer