#12 クラスのコピーの仕方3 STLコンテナを使うとき

Effective C++ 12項の内容+αの内容になりますが,STLのコンテナを使うときにコピーコンストラクタ・コピー代入演算子がどのように働くかを見てみます.

下記のサンプルコードを作ってみました.

#include <iostream>
#include <memory>
#include <cstring>
#include <vector>

class MyContainer {
public:

  MyContainer() 
    : a(0), b(0)
  {
    std::cout << "MyContainer()" << std::endl;
  }

  ~MyContainer()
  {
    std::cout << "~MyContainer()" << std::endl;
  }

  MyContainer(const MyContainer& obj) 
    : a(obj.a), b(obj.b)
  {
    std::cout << "MyContainer(const MyContainer& obj)" << std::endl;    
  }

  MyContainer& operator=(const MyContainer& obj) {
    std::cout << "MyContainer& operator=(const MyContainer& obj)" << std::endl;
    if (this != &obj) {
      this->a = obj.a;
      this->b = obj.b;
    }
    return *this;
  }

  int a, b;
};

int main(int, char**) {

  std::cout << __FILE__ << std::endl;

  MyContainer cont1;

  std::cout << std::endl;
  std::cout << "Start of vector scope." << std::endl;
  {
    std::vector<MyContainer> vec;
    vec.push_back(cont1);
  }
  std::cout << "End of vector scope." << std::endl;
  std::cout << std::endl;

  return 0;
}

上記のコードを実行すると,下記の結果になります.

MyContainer()

Start of vector scope.
MyContainer(const MyContainer& obj)
~MyContainer()
End of vector scope.

~MyContainer()

で,上記を見てわかると思うのですが,STLのベクタに要素をプッシュした場合,コピーコンストラクタが呼ばれていることがわかります.で,vectorのが定義されたスコープの外に出ると,vectorが廃棄されるときにvectorに入れられている要素のデストラクタが呼ばれていることがわかります.ということで,自作オブジェクトに対してSTLを使用する場合,,,

1.コピー戦略として,ディープコピーを採用しているオブジェクトならそのまま使える.(ディープコピーがちゃんと実装されていればですが..)

2.コピー戦略として,シャローコピーを採用しているオブジェクトの場合,共有している領域のリークや二重解放が起こらないようにかなり慎重に作る必要がある.

という感じでしょうか.

#12 クラスのコピーの仕方2 ディープコピー

ということで,Effective C++ 12項の内容の続きですが,ここでは自作コンテナを Deep Copy する例を取り上げます.

ディープコピーのイメージ

f:id:rkoichi2001:20181215151639j:plain
ディープコピーのイメージ

ディープコピーした場合は,コピー元のオブジェクトとコピー先のオブジェクトになんの関係もないので,コピー後に片方を変更しても他方には影響がありません.おそらく,普通にオブジェクトをコピーした時とか,コピー代入演算子を使った場合はこちらの挙動をイメージする場合が多いのではないかなと思います.

intとかdoubleとかの組み込み型の挙動を考えてみても,,,,

int a, b;
a = 10
b = 20;

b = a;
a = 1000;

当然ながら,aの代入後もbは20のままです.この感覚から行くと,AとBというオブジェクトがあったとして,

Object A(hoge1), B(hoge2);
B = A
A = xxx

とやっても,Bは"A = xxx"の変更の影響を受けない.ただ,cv::Matみたいにケースバイケースではあると思うので,使う側も要注意ですね.

下記,ディープコピーを実現するサンプルを作ってみました.

#include <iostream>
#include <memory>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~DeepCopyContainer()
  {
    delete[] this->arr;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << "chap_2nd_12_5" << std::endl;

  // 生のポインタを持つ Container.単体としての挙動は成立.
  DeepCopyContainer d1, d2;

  return 0;
}

で,ディープコピーを実現するサンプルです.下記のようになにも考えずにポインタを持つコンテナを作ってしまうと,コピー代入演算子を呼んだ時に,メモリリークしてしまいます.

#include <iostream>
#include <memory>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~DeepCopyContainer()
  {
    delete[] this->arr;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << "chap_2nd_12_5" << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1, d2;

  // デフォルトのコピー代入演算子を使ってしまうと,メモリリーク...
  d1 = d2;

  return 0;
}

また,デフォルトのコピーコンストラクタを呼び出すと,メモリの二重解放が起こります....


#include <iostream>
#include <memory>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~DeepCopyContainer()
  {
    delete[] this->arr;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << "chap_2nd_12_5" << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1;

  // デフォルトのコピーコンストラクタを使ってしまうと,最後に二重解放.
  DeepCopyContainer d2(d1);

  return 0;
}

<||

ということで,自作コピーコンストラクタとコピー代入演算子を作成します.

>|cpp|

#include <iostream>
#include <memory>
#include <cstring>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {
    std::cout << "DeepCopyContainer()" << std::endl;
  }

  ~DeepCopyContainer()
  {
    std::cout << "~DeepCopyContainer()" << std::endl;
    delete[] this->arr;
  }

  DeepCopyContainer(const DeepCopyContainer& obj)
    : arr(new int[ARR_LENGTH])
  {
    std::cout << "DeepCopyContainer(const DeepCopyContainer& obj)" << std::endl;
    std::memcpy(this->arr, obj.arr, ARR_LENGTH * sizeof(int));
  }

  DeepCopyContainer& operator=(const DeepCopyContainer& obj) {
    std::cout << "DeepCopyContainer& operator=(const DeepCopyContainer& obj)" << std::endl;    
    // 自己代入チェック.
    if (this != &obj) {
      std::memcpy(this->arr, obj.arr, ARR_LENGTH * sizeof(int));
    }

    // 自分を返す.
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << __FILE__ << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1;

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    d1.arr[i] = 99;
  }

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d1.arr[i] << ", ";
  }
  std::cout << std::endl;

  // コピーコンストラクタの挙動チェック.
  DeepCopyContainer d3(d1);
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d3.arr[i] << ", ";
  }

  // コピー代入演算子の挙動チェック
  DeepCopyContainer d4;
  d4 = d1;
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d4.arr[i] << ", ";
  }

  return 0;
}

もしくは,SmartPointerを使って下記のようにも実装できます.

#include <iostream>
#include <memory>
#include <cstring>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {
    std::cout << "DeepCopyContainer()" << std::endl;
  }

  ~DeepCopyContainer()
  {
    std::cout << "~DeepCopyContainer()" << std::endl;
  }

  DeepCopyContainer(const DeepCopyContainer& obj)
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {
    std::cout << "DeepCopyContainer(const DeepCopyContainer& obj)" << std::endl;
    std::memcpy(this->arr.get(), obj.arr.get(), ARR_LENGTH * sizeof(int));
  }

  DeepCopyContainer& operator=(const DeepCopyContainer& obj) {
    std::cout << "DeepCopyContainer& operator=(const DeepCopyContainer& obj)" << std::endl;    
    // 自己代入チェック.
    if (this != &obj) {
      std::memcpy(this->arr.get(), obj.arr.get(), ARR_LENGTH * sizeof(int));
    }

    // 自分を返す.
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {

  std::cout << __FILE__ << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1;

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    d1.arr.get()[i] = 99;
  }

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d1.arr.get()[i] << ", ";
  }
  std::cout << std::endl;

  // コピーコンストラクタの挙動チェック.
  DeepCopyContainer d3(d1);
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d3.arr.get()[i] << ", ";
  }

  // コピー代入演算子の挙動チェック
  DeepCopyContainer d4;
  d4 = d1;
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d4.arr.get()[i] << ", ";
  }

  return 0;
}

ということで,まとめとして...

1.生のポインタを持つクラスを作るときは,コピーの戦略を考える.

2.生のポインタでなく,出来る限りスマートポインタを使う.

3.オブジェクトを内包するクラスを作るときは,内包するオブジェクトのコピー戦略を調べて,自分の意図とあっているかチェックする.

#12 クラスのコピーの仕方1 シャローコピー

ということで,Effective C++の12項の内容なんですが,この辺自分もきちんと理解しきれてなかったのでまとめてみました.

コピーの種類

オブジェクトをデザインするときに,コピーの振る舞いとか定義をどうするかを決めないといけないと思うんですが,大まかにいってコピーした時の振る舞いって2つあると思います.

シャローコピー

オブジェクトAとオブジェクトBが2つあったとして,"B = A" とか "Hoge B(A)" とかしたときに,オブジェクトAの領域の一部がオブジェクトBとシェアされます.OpenCV の cv::Mat とかは単純に = でコピーすると画像領域自体はシェアされます.オブジェクト内部に持っているデータ領域がとても大きくて,「別のメモリを確保して,全部コピーして」っていうのが不自然な場合はこちらで実装するのかと思います.シャローコピーの場合だと,オブジェクトAに対する変更がオブジェクトBに影響を与えます.

手書きで恐縮ですが,イメージとしては下記のような感じですかね.

f:id:rkoichi2001:20181215151512j:plain
シャローコピーのイメージ

ディープコピー

オブジェクトAとオブジェクトBが2つあったとして,"B = A" とか "Hoge B(A)" とかしたときに,オブジェクトAの領域とオブジェクトBの領域は完全に別物になります.OpenCVの cv::Mat では,これをやりたければ cv::Mat::clone() という関数を呼べば完全に別物の cv::Mat が作れる様になっています.あと,std::vector なんかも単純に "=" ってやると完全に別のオブジェクト(領域を全く共有してない)ができます.ディープコピーの場合だと,オブジェクトAに対する変更はオブジェクトBに全く影響を与えません.

こちらもイメージとしては下記のような感じですかね.

f:id:rkoichi2001:20181215151639j:plain
ディープコピーのイメージ

どういうときに気を付けるか?

で,実際に自分のオブジェクトを作るとき,どういうときに上記のような話に気を付けてないといけないか...ということですが,おそらく下記の2点に注意する必要があると思います.

1.自分のデザインするオブジェクトにメンバとしてポインタを持たせる場合.
2.自分のデザインするオブジェクトにメンバとしてほかのオブジェクトを持たせる場合.

1の ”自分のデザインするオブジェクトにメンバとしてポインタを持たせる場合.” ですが,こちらに関しては何も考えずに作ると「シャローコピー」&「メモリリーク」します....2の他のオブジェクトを内包するコンテナを作る場合,内包するオブジェクトのコピー戦略がどうなっているかちゃんと調べる必要があります.例えば,OpenCVのcv::Matをメンバとして持つコンテナを作る場合,デフォルトのコピー代入演算子を使ってコンテナのコピーをした場合,cv::Matの指すメモリ領域は共有されます.これが自然なばあいはこれでOKですが,ディープコピーが必要な場合は cv::Mat::clone() を呼ぶ等して適当なコピー代入演算子を作ってあげる必要があります.

まずはシャロコピーのサンプルのコードとして,下記を作って見ました.

#include <iostream>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~ShallowCopyContainer()
  {
    delete[] arr;
  }

public:
  static const int ARR_LENGTH = 100;

private:
  int* arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA, contB;
  contB = contA;

  return 0;
}

上記のコードを下記の様に実行します.

valgrind --leak-check=full ./chap_2nd_12_1

すると,ガッツリメモリリークします.

==4928== Memcheck, a memory error detector
==4928== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==4928== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==4928== Command: ./chap_2nd_12_1
==4928==
==4928== Invalid free() / delete / delete[] / realloc()
==4928==    at 0x4C2F74B: operator delete[](void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CD8: ShallowCopyContainer::~ShallowCopyContainer() (chap_2nd_12_1.cpp:12)
==4928==    by 0x400C01: main (chap_2nd_12_1.cpp:25)
==4928==  Address 0x5ab6c80 is 0 bytes inside a block of size 400 free'd
==4928==    at 0x4C2F74B: operator delete[](void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CD8: ShallowCopyContainer::~ShallowCopyContainer() (chap_2nd_12_1.cpp:12)
==4928==    by 0x400BF5: main (chap_2nd_12_1.cpp:25)
==4928==  Block was alloc'd at
==4928==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CA3: ShallowCopyContainer::ShallowCopyContainer() (chap_2nd_12_1.cpp:7)
==4928==    by 0x400BD0: main (chap_2nd_12_1.cpp:25)
==4928==
==4928==
==4928== HEAP SUMMARY:
==4928==     in use at exit: 73,104 bytes in 2 blocks
==4928==   total heap usage: 3 allocs, 2 frees, 73,504 bytes allocated
==4928==
==4928== 400 bytes in 1 blocks are definitely lost in loss record 1 of 2
==4928==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CA3: ShallowCopyContainer::ShallowCopyContainer() (chap_2nd_12_1.cpp:7)
==4928==    by 0x400BDC: main (chap_2nd_12_1.cpp:25)
==4928==
==4928== LEAK SUMMARY:
==4928==    definitely lost: 400 bytes in 1 blocks
==4928==    indirectly lost: 0 bytes in 0 blocks
==4928==      possibly lost: 0 bytes in 0 blocks
==4928==    still reachable: 72,704 bytes in 1 blocks
==4928==         suppressed: 0 bytes in 0 blocks
==4928== Reachable blocks (those to which a pointer was found) are not shown.
==4928== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==4928==
==4928== For counts of detected and suppressed errors, rerun with: -v
==4928== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

で,”何がいけなかったのか?”ですが,C++でデフォルト生成されるコピー代入演算子は組み込み型の変数を単純コピーします.なので,"int* arr" というポインタ変数に関しても単純にアドレスを contB オブジェクトのアドレス変数にコピーします.この振る舞いに関しては,シャローコピーでいいなら問題はないはずです.で,contAとcontBで同じ領域がシェアされて,めでたしめでたし....となるはずなんですが,上の作りだと contB にもともと確保されていた領域 "new int[ARR_LENGTH]" への参照が全くなくなってしまい,メモリリークしてしまいます.つまり,デフォルトの代入演算子だとまずいわけです....では,ちょっと書きなおしてみます.

#include <iostream>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~ShallowCopyContainer()
  {
    if (arr != nullptr) {
      delete[] arr;
      arr = nullptr;
    }
  }

  ShallowCopyContainer& operator=(const ShallowCopyContainer& rhs) {

    // 自分だった場合は,そのまま自分を返す.
    if (this == &rhs) {
      return *this;
    }

    // もともと自分が持っていた領域を delete[] してリークを防ぐ.
    delete[] this->arr;
    this->arr = rhs.arr;
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

private:
  int* arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA, contB;
  contB = contA;

  return 0;
}

上記の変更で,コピー代入演算子が呼ばれた時には自分が持っていた領域を解放するようになるので,メモリリークはなくなりました.ただ,,,まだ二重解放の問題が残っています.というのも,デストラクタで必ず delete が呼ばれるので,contA のデストラクタが呼ばれた時に解放された領域を,contB のデストラクタが再度解放してしまおうとするからです.参照カウントとか導入するとどんどん話が難しくなっていくので,もっと簡単に対処できる方法を模索してみたかったんですが,,,結局 Smart Pointer を使っちゃいます(笑)

#include <iostream>
#include <memory>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {}

  ~ShallowCopyContainer()
  {}

  ShallowCopyContainer& operator=(const ShallowCopyContainer& rhs) {

    // 自分だった場合は,そのまま自分を返す.
    if (this == &rhs) {
      return *this;
    }

    this->arr = rhs.arr;
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA, contB;
  contB = contA;

  for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
    contA.arr.get()[i] = 99;
  }

  for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
    std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
    std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
  }

  return 0;
}

結局 STL のパワーに頼ってしまいましたが,上記の実装で Container A の中身を変えれば Container B の中身も変わる”シャローコピー”ができました.で,次にもうひとつ,コピーコンストラクタの存在も忘れてはいけません.と入っても,結局コピー代入演算子の時に対面したような問題が出てくるだけなので,それを再度踏まないように実装してあげる必要があります.ということで,結局スマートポインタに頼ります.

#include <iostream>
#include <memory>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {}

  ShallowCopyContainer(const ShallowCopyContainer& obj)
    : arr(obj.arr)
  {}

  ~ShallowCopyContainer()
  {}

  ShallowCopyContainer& operator=(const ShallowCopyContainer& rhs) {

    // 自分だった場合は,そのまま自分を返す.
    if (this == &rhs) {
      return *this;
    }

    this->arr = rhs.arr;
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA;
  ShallowCopyContainer contB(contA);

  for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
    contA.arr.get()[i] = 99;
  }

  for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
    std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
    std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
  }

  return 0;
}

で,ここでちょっと気づいたんですが,SmartPointer使っていれば,自分でコピー代入演算子・コピーコンストラクタを定義しなくてもいいのでは?とおもったんで,やってみました.結果,ビンゴでした.シャローコピーを実現するだけなら,SmartPointerを使ってやればコピー代入演算子もコピーコンストラクタも自作する必要はありませんでした.

#include <iostream>
#include <memory>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {}

  ~ShallowCopyContainer()
  {}

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {
  
  {
    ShallowCopyContainer contA;
    ShallowCopyContainer contB(contA);

    for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
      contA.arr.get()[i] = 99;
    }

    for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
      std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
      std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
    }
  }

  {
    ShallowCopyContainer contA, contB;
    contB = contA;

    for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
      contA.arr.get()[i] = 101;
    }

    for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
      std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
      std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
    }
  }

  return 0;
}


ということで,シャローコピーを実現する自作コンテナの実装方法でした.注意する点としては,,,,

コンテナが生のポインタを持つ場合は,"コピー代入演算子"と"コピーコンストラクタ"の実装が必須!

スマートポインタを使ってやれば,シャローコピーでいいならデフォルトのコピーコンストラクタ・コピー代入演算子が使える.

#35 仮想関数の代わりになるものを考える

Effective C++ #35 の内容です.この項はちょっと一風変わっていて,継承と仮想関数の仕組みを用いたシンプルなポリモーフィズムの実装を,異なるデザインパターンでおきかえる話でした.登場したデザインパターンとしては,

1.非仮想のインターフェースを使うテンプレートメソッドパターン
2.関数ポインタを使うストラテジパターン
3.tr1::functionによるストラテジパターン
4.古典的なストラテジパターン

の4つでした.サンプルコードを通して,4つのパターンを見ていきたいと思います.

0.シンプルに仮想関数を使って実装してみる.

下記,Lidarを持つロボットとCameraを持つロボットで環境認識の方法が変わるので,Robotという基底クラスに対してLidarRobot, CameraRobotというクラスを派生させています.う〜ん.我ながら,このクラス設計はいけてないですね(笑).ストラテジパターンを使えばうまくまとまるのかしら.

#include <iostream>

class Robot {
public:
  virtual ~Robot(){}
  virtual double sense() const = 0;
};

class CameraRobot : public Robot {
public:
  virtual ~CameraRobot(){}
  virtual double sense() const {
    std::cout << "Sensing by Camera." << std::endl;
    return 0.0;
  };
};

class LidarRobot : public Robot {
public:
  virtual ~LidarRobot(){};
  virtual double sense() const {
    std::cout << "Sensing by Lidar." << std::endl;
    return 0.0;
  };
};

int main(int, char const **)
{

  const Robot* robo = new CameraRobot();
  robo->sense();

  return 0;
}

1.非仮想のインターフェースを使うテンプレートメソッドパターン.

次に,非仮想のインターフェースをパターンですが,こいつは0のケースとそんなに変わってないですね.virtualな関数をprivateにして,上からpublicのテンプレートメソッドから呼ぶようにしただけです.仮想関数はprivateで有るべきという考え方が元になっているパターンのようで,NVI(Non-virtual interface)イディオムとよぶらしいです.

#include <iostream>

class Robot {
public:
  virtual ~Robot(){}
  double sense() const {
    return this->doSense();
  }

private:
  virtual double doSense() const = 0;
};

class CameraRobot : public Robot {
public:
  virtual ~CameraRobot(){}
private:
  virtual double doSense() const {
    std::cout << "Sensing by Camera." << std::endl;
    return 0.0;
  };
};

class LidarRobot : public Robot {
public:
  virtual ~LidarRobot(){};
private:
  virtual double doSense() const {
    std::cout << "Sensing by Lidar." << std::endl;
    return 0.0;
  };
};

int main(int, char const **)
{

  std::cout << "Hello World!" << std::endl;

  const Robot* robo = new CameraRobot();
  robo->sense();


  return 0;
}

2.関数ポインタを使うストラテジパターン.

ここからの3つはストラテジパターンです.おそらくこの例のパターンはストラテジパターンとして実装すべき内容なのだと思います.


#include <iostream>

double cameraSense() {
  std::cout << "CameraSensing" << std::endl;
  return 0.0;
}

double lidarSense() {
  std::cout << "LidarSensing" << std::endl;
  return 0.0;
}

class Robot {
public:
  typedef double (*senseFunc)(void);
  explicit Robot(senseFunc func) 
  : func(func)
  {}
  double sense() const {
    return this->func();
  }
private:
  senseFunc func;
};

class Humanoid : public Robot {
public:
  explicit Humanoid(senseFunc func) 
    : Robot(func)
  {}
};

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  const Robot*robo = new Humanoid(cameraSense);
  robo->sense();

  return 0;
}

3.tr1::functionによるストラテジパターン.

std::tr1::functionはいろんなことができるみたいですね...ちょっとここでやりだすと終わらないので,また別の機会にやります.基本的には,関数ポインタをstd::tr1::functionをつかって表現するように変更されただけです.

#include <iostream>
#include <tr1/functional>

double cameraSense(void) {
  std::cout << "Camera Sense" << std::endl;
  return 0.0;
}

class Robot {
public:
  typedef std::tr1::function<double (void)> senseFunc;
  explicit Robot(senseFunc func) 
  : func(func)
  {};
  virtual ~Robot() {}
  double doSense() const {
    func();
    return 0.0;
  }

private:
  senseFunc func;
};

class Humanoid : public Robot {
public:
  explicit Humanoid(senseFunc func) 
    : Robot(func)
  {}
};

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  const Robot* robo = new Humanoid(cameraSense);
  robo->doSense();

  return 0;
}

4.古典的なストラテジパターン.

クラス階層の中の仮想関数を別のクラス階層 ”SensingStrategy” の仮想関数で置き換えました.

#include <iostream>

class SensingStrategy {
public:
  virtual ~SensingStrategy() {}
  virtual double sense() const = 0;
};

class CameraSensing : public SensingStrategy {
public:
  virtual ~CameraSensing() {}
  virtual double sense() const {
    std::cout << "CameraSensing" << std::endl;
    return 0.0;
  }
};

class LidarSensing : public SensingStrategy {
public:
  virtual ~LidarSensing() {}
  virtual double sense() const {
    std::cout << "LidarSensing" << std::endl;
    return 0.0;
  }
};

class Robot {
public:
  explicit Robot(std::string strategy) 
  : sStrat(nullptr)
  {
    if (strategy == "Camera") {
      sStrat = new CameraSensing();
    }
  };
  virtual ~Robot() {}
  double doSense() const {
    sStrat->sense();
    return 0.0;
  }

private:
  SensingStrategy *sStrat;
};

class Humanoid : public Robot {
public:
  explicit Humanoid(std::string strategy) 
    : Robot(strategy)
  {}
};

int main(int, char**) {

  std::cout << "Hello World!" << std::endl;
  const Robot* robo = new Humanoid("Camera");
  robo->doSense();

  return 0;
}

SLAMのお勉強②−2 ~GMapping の環境設定

ということで,実際に ROS GMapping の解剖実験をするにあたって,環境を整えます.ROSのモジュールとして「ROSラッパー」「アルゴコア」の両方がきちんと管理されているので,そのまま catkin のパッケージとして落としてくれば,あとは catkin_make でビルドが通ると思うのですが,catkin で全部ビルドすると時間がかかるような気がしているのと,いろいろとうまいことやってくれるので全体像が見えづらくなってしまうかな...とおもい,アルゴコアは catkin の枠組みに頼らず,普通にライブラリとしてコンパイルし,そのライブラリを ROSラッパーから見に行くようにしました.

0.既存パッケージの削除

このステップは必ずしも必要ないかもしれないですが,解剖実験でソフトを変更した時に,既存パッケージのヘッダファイルを参照してしまうといろいろとうまく行かなくなってしまうので,いちおう削除しておきます.

sudo apt-get purge ros-kinetic-slam-gmapping
sudo apt-get purge ros-kinetic-gmapping
sudo apt-get purge ros-kinetic-openslam-gmapping

上記のコマンドで,gmapping 関連の必須モジュールは削除できるかと思います.

1.アルゴコア/ROSラッパーのFork

GIT-HUBで管理されている下記のリポジトリをGIT-HUBにフォークしました.どうしてもコードを読むだけだとイマイチ頭に入ってこないので,ソースリファクタしたり,printfデバッグしたりしようとおもっているんですが,これを保存しておきたく.
アルゴコア
github.com

ROSラッパー
github.com

2.catkin を使わずに,アルゴコアのビルド

catkinを使わずにビルドしたいので,CMakeLists.txt を書き換えます.
github.com

上の変更は下記のフォルダ構成前提になってます.

top_directory
 - install
 - build
 - openslam_gmapping 

で,後は

cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../install ../openslam_gmapping/
make -j32
make install

コンパイルすると,installディレクトリに各種ヘッダとライブラリが install ディレクトリ配下に生成されます.

3.ROSラッパーを catkin_ws/src配下にダウンロードし,コンパイルする.

ROSのソースディレクトリ”catkin_ws/src”配下にROSラッパーをダウンロードします.つまり...
catkin_ws/src配下で,,,

git clone https://github.com/ros-perception/slam_gmapping.git

で,openslam_gmappingを catkin のフレームワークを使わずにコンパイルしたので,CMakeLists.txt を少し変更します.

ひとまずここまでくれば,マイ gmapping 環境の出来上がりです.次のエントリでは解剖した内容を少しずつ書いていきます.

SLAMのお勉強②−1 ~GMapping vs Google Cartographer

下記のエントリでSLAMの勉強をしているという話をしましたが,論文&本読みだけだとなかなか具体的なイメージが湧いてこないので,ソースコードも引っ張ってきて勉強することにしました.で,題材には下記の2つを使おうと思います.

daily-tech.hatenablog.com

GMapping

皆様ご存知,ROSのデフォルトマッピングアルゴリズムとして使われているGMappingです.実装のベースとなる論文は,,,,

論文

Improving Grid-based SLAM with Rao-Blackwellized Particle Filters by Adaptive Proposals and Selective Resampling
著者:Giorgio Grisetti, Cyrill Stachniss, Wolfram Burgard

実装

で,ROSのコンパイル済パッケージとして使っている人がほとんどだと思うんですが,コンパイル済のバイナリでは何も勉強できないので,ソースコードをてにいれる必要があります.実装はROSのラッパー部分とアルゴコアの2つに分かれていて,下記からダウンロードできます.

アルゴコア

github.com

ROSラッパー

github.com

実行結果

下記,今年のつくばチャレンジ確認コースに対してGMappingを使ってマッピングした様子です.
youtu.be

Google Cartographer

もうひとつはGoogleのチームが開発・保守しているCartographerです.

論文

Real-Time Loop Closure in 2D LIDAR SLAM
著者:Wolfgang Hess, Damon Kohler, Holger Rapp, Daniel Andor

実装

こちらもソースコードを下記からてにいれます.

アルゴコア

github.com

ROSラッパー

github.com

実行結果

下記,今年のつくばチャレンジ確認コースに対してCartographerを使ってマッピングした様子です.
youtu.be


う〜ん.Cartographerのほうが正しくマッピングできるはずなんですが,GMappingのほうがこのデータに関しては綺麗にマッピングできました.おそらく,毎サイクルLidarのデータをつかって最適化計算しているので,ロボットに対するLidarの設置位置精度がかなりシビアなのと,路面の傾きがあるとよろしくないのかと思います.

ということで,ここからは両方のアルゴリズムに関して技術的詳細に突っ込んでいきます.

#3(の2)可能ならいつでも const を使う.

Effective C++ 3項の内容です.

const なメンバ関数とは??

前回のエントリで変数に対する const の説明をしました.今回は,関数に対して使う const の説明です.クラスのメンバ関数に対して const をつけることによって,そのメソッドが当該オブジェクトに対して何も変更をもたらさないことを宣言できます.サンプルコード全体は最後でのせるとして,細かな説明をしてみます.まず,ConstSampleのメンバ関数として,下記のconstFunctionを宣言しました.このメンバ関数の後ろに const がついていますが,これの意味するところは,「この関数はこのオブジェクトの状態を何も変更しません!」ということです.もしこの関数がオブジェクトの状態を変更してしまおうとしても,コンパイルが通りません.

  void constFunction() const;

ということで,いくつか実験してみました.

1.クラスのメンバ変数を思いっきりダイレクトに代入してみる.
"error: assignment of member ‘ConstSample::m_x’ in read-only object" というエラーメッセージでコンパイルが失敗します.

void ConstSample::constFunction() const {
  m_x = 10;
}

2.クラスのメンバオブジェクトのメソッドをそれとなく呼んでみる.
ConstSampleには,vectorをメンバとしてもたせました.vector::clearやpush_backは明らかにvectorの状態を変えてしまいますが,これもきちんとコンパイルエラーになります.vectorメンバ関数として定義されている clear や push_back はそれ自身が const として宣言されていません.このような関数は文字通り vector の状態をかえてしまう可能性があります.コンパイラはこの情報を手がかりに,この関数を呼び出すと状態を変更してしまう可能性があることを知り,コンパイルを失敗させます.
"error: passing ‘const std::vector’ as ‘this’ argument discards qualifiers [-fpermissive]"

  vec.clear();
  vec.push_back(10);

3.クラスのメンバ変数(ポインタ)が指す領域の値を変更する.
Effective C++でも,問題として取り上げられていましたが,これはコンパイルエラーにはなりません.あくまでもポインタ変数(=アドレスを持つ変数)がメンバなのであって,ポインタ変数の指す先の値がどうなろうが知ったこっちゃない!ということみたいです.下記,サンプルコードでの実験結果とも一致します.

  m_y[2] = 10;

4.const 定義されていない他のメンバ関数を呼び出す.
これはコンパイルエラーになります.理由としては,const宣言されていない関数を呼び出すとその関数がオブジェクトの状態を変更してしまう可能性があるからです.
"error: passing ‘const ConstSample’ as ‘this’ argument discards qualifiers [-fpermissive]"

  someFunction();

5.const 定義されていない参照変数としてメンバを返すメソッド.
これもコンパイルエラーになります.理由としては,参照変数を返した先の未知世界で参照変数が更新されるかもしれない(=オブジェクトの状態が変わってしまう)からです.
"error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers"

int& ConstSample::getVal() const {
  return m_x;
}

ちなみに,const int& を返す関数として下記のように定義すると,コンパイルが通るようになります.

const int& ConstSample::getVal() const {
  return m_x;
}

こうやって実験してみると,これ便利ですね.確かに,可能ならいつでも const つけると無駄な混乱を避けてとても使いやすくなりそうです.

#include <iostream>
#include <vector>

class ConstSample {
public:
  ConstSample() 
    : m_x(0), vec()
  {
    m_y = new int[10];
  }

  ~ConstSample()
  {
    delete[] m_y;
  }

  void constFunction() const;

  void nonConstFunction();

  void someFunction();

  const int& getVal() const;

private:
  int m_x;
  int *m_y;
  std::vector<int> vec;
};

void ConstSample::constFunction() const {
  // error: assignment of member ‘ConstSample::m_x’ in read-only object
  // m_x = 10;

  // error: passing ‘const std::vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
  // vec.clear();

  // error: passing ‘const std::vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
  // vec.push_back(10);

  m_y[2] = 10;

  // error: passing ‘const ConstSample’ as ‘this’ argument discards qualifiers [-fpermissive]
  // someFunction();
}

void ConstSample::nonConstFunction() {
  m_x = 10;

  vec.clear();

  vec.push_back(10);

  m_y[2] = 10;
}

void ConstSample::someFunction() {

}

const int& ConstSample::getVal() const {
  return m_x;
}

int main(int, char**) {

  std::cout << "Effective C++ 3項" << std::endl;

  return 0;
}

オーバーロードを用いた const / 非const な関数の呼び分け

C++は同名でシグニチャが異なる複数の関数を宣言できました.これは const / 非 const でも同じように適用できます.つまり,

同名の関数を const / 非 const の違いだけで宣言できる.

で,実際に呼び出されるときは const / 非 const を見て,合致した関数が呼び出されます.下記,サンプルコードです.[]演算子を宣言しましたが,const バージョンと 非 const バージョンの2パターン用意しました.この関数がどのように呼び分けられるかを,下記の2つの関数で試します.

void printSampleVector(const SampleVector& samVec) {
  std::cout << samVec[1] << std::endl;
}

void modifySampleVector(SampleVector& samVec) {
  samVec[1] = 100;
}

実際の実行結果は下記のようになりましたが,const バージョンと 非 const バージョンの operator[] がきちんと呼び分けられている様子がわかりました.

Effective C++ 3項
Calling printSampleVector(const SampleVector&) <- const 参照渡しをしているので,呼び出される operator[] は const バージョン.
Const version of operator[]
1
Calling modifySampleVector(SampleVector&) <- ただの参照渡しをしているので,呼び出される operator[] は 非const バージョン.
Non const version of operator[]
Calling SampleVector::pringAllElems
0
100
2


ということで,2つ目のサンプルコードです.

#include <iostream>
#include <vector>

class SampleVector {
public:
  SampleVector()
  {
    vec.push_back(0);
    vec.push_back(1);
    vec.push_back(2);
  }

  ~SampleVector()
  {}

  int& operator[](int pos) {
    std::cout << "Non const version of operator[]" << std::endl;
    return vec[pos];
  }

  const int& operator[](int pos) const {
    std::cout << "Const version of operator[]" << std::endl;
    return vec[pos];
  }

  void printAllElems() const {
    for (unsigned int i = 0; i < vec.size(); i++) {
      std::cout << vec[i] << std::endl;
    }
  }

private:
  std::vector<int> vec;
};

void printSampleVector(const SampleVector& samVec) {
  std::cout << samVec[1] << std::endl;
}

void modifySampleVector(SampleVector& samVec) {
  samVec[1] = 100;
}

int main(int, char**) {

  std::cout << "Effective C++ 3項" << std::endl;

  SampleVector samVec;

  std::cout << "Calling printSampleVector(const SampleVector&)" << std::endl;
  printSampleVector(samVec);

  std::cout << "Calling modifySampleVector(SampleVector&)" << std::endl;
  modifySampleVector(samVec);

  std::cout << "Calling SampleVector::pringAllElems" << std::endl;
  samVec.printAllElems();

  return 0;
}