調べたいこと。
・オブジェクトがコピーまたは代入されるときの挙動
・std::vector#push_back や std::vector#back での挙動
・std::map#[] での挙動

サンプルコード copytest.cpp


#include <iostream>
#include <string>
#include <vector>
#include <map>
 
class Hoge {
public:
  // デフォルト コンストラクタ
  Hoge(){
    this->name = "xxx";
    std::cout << "default constructor: " << this->name << std::endl;
  };
  // コンストラクタ
  Hoge(std::string name){
    this->name = name;
    std::cout << "constructor: " << this->name << std::endl;
  };
  // コピー コンストラクタ
  Hoge(const Hoge& hoge){
    this->name = hoge.name;
    std::cout << "copy constructor: " << this->name << std::endl;
  };
  // 代入演算子のオーバーロード
  Hoge& operator=(const Hoge& hoge){
    this->name = hoge.name;
    std::cout << "assignment operator: " << this->name << std::endl;
  }
  void PrintName(){
    std::cout << "PrintName: " << this->name << std::endl;
  };
  virtual ~Hoge(){
  };
private:
  std::string name;
};
 
int main(){
 
  std::cout << "\n***** h1 test *****" << std::endl;
  Hoge h1("Alice"); // 普通のコンストラクタ
  h1.PrintName();
 
  std::cout << "\n***** h2 test *****" << std::endl;
  Hoge h2 = h1; // コピーコンストラクタ
  h2.PrintName();
 
  std::cout << "\n***** h3 test *****" << std::endl;
  Hoge h3("Bob");
  h3.PrintName();
  h3 = h1; // 代入
  h3.PrintName();
 
  std::cout << "\n***** h4 test *****" << std::endl;
  std::vector<Hoge> v1;
  v1.push_back(h1); // std::vector#push_back でコピーが発生
 
  std::cout << "\n***** h5 test *****" << std::endl;
  Hoge& h5 = v1.back(); // 参照を返す
  h5.PrintName();
 
  std::cout << "\n***** h6 test *****" << std::endl;
  Hoge* h6 = &v1.back(); // 参照のポインタ
  h6->PrintName();
 
  std::cout << "\n***** h7 test *****" << std::endl;
  std::map<std::string, Hoge> m1;
  m1["key1"]; // 存在しないキーを指定したためデフォルトコンストラクタが呼ばれる
 
  std::cout << "\n***** h8 test *****" << std::endl;
  m1["key1"] = h1; // 代入
 
  std::cout << "\n***** h9 test *****" << std::endl;
  m1["key1"].PrintName();
 
  std::cout << "\n***** h10 test *****" << std::endl;
  m1["key2"].PrintName(); // 存在しないキーを指定したためデフォルトコンストラクタが呼ばれる
 
  std::cout << "\n***** h11 test *****" << std::endl;
  Hoge& h11 = m1["key1"]; // 参照を返す
  h11.PrintName();
 
  std::cout << "\n***** h12 test *****" << std::endl;
  Hoge h12 = m1["key1"]; // コピーが発生
  h12.PrintName();
 
  std::cout << "\n***** h13 test *****" << std::endl;
  m1["key3"] = h1; // 存在しないキーを指定したためデフォルトコンストラクタが呼ばれ、その後で代入が発生
 
   return 0;
}

実行結果。


$ g++ copytest.cpp 
$ ./a.out 
 
***** h1 test *****
constructor: Alice
PrintName: Alice
 
***** h2 test *****
copy constructor: Alice
PrintName: Alice
 
***** h3 test *****
constructor: Bob
PrintName: Bob
assignment operator: Alice
PrintName: Alice
 
***** h4 test *****
copy constructor: Alice
 
***** h5 test *****
PrintName: Alice
 
***** h6 test *****
PrintName: Alice
 
***** h7 test *****
default constructor: xxx
copy constructor: xxx
copy constructor: xxx
 
***** h8 test *****
assignment operator: Alice
 
***** h9 test *****
PrintName: Alice
 
***** h10 test *****
default constructor: xxx
copy constructor: xxx
copy constructor: xxx
PrintName: xxx
 
***** h11 test *****
PrintName: Alice
 
***** h12 test *****
copy constructor: Alice
PrintName: Alice
 
***** h13 test *****
default constructor: xxx
copy constructor: xxx
copy constructor: xxx
assignment operator: Alice

std::map[key] でデフォルトコンストラクタが呼ばれるのは良いとして、その後2回もコピーコンストラクタが呼ばれているのはどういうわけなんだろうか。

ちなみに環境は Mac OS X Lion と g++ 4.2.1 を使用。


$ uname -mrsv
Darwin 11.3.0 Darwin Kernel Version 11.3.0: Thu Jan 12 18:47:41 PST 2012; root:xnu-1699.24.23~1/RELEASE_X86_64 x86_64
 
$ g++ --version
i686-apple-darwin11-llvm-g++-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.9.00)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

参考資料。

コピー操作が行われるときにコピーコンストラクタが呼び出されるということでした。通常のコンストラクタと同じく、明示的にコピーコンストラクタを宣言しなかった場合には、コンパイラが暗黙のうちにコピーコンストラクタを生成しています。これをデフォルトコピーコンストラクタと呼びますが、これは何をしているのでしょう? これは単にクラスのメンバをそのままコピーするだけです。つまり、単純な代入操作です。この章で、何度か話した、ポインタがそのままコピーされるというのは、実はデフォルトコピーコンストラクタがそうさせているのです。

C++編(言語解説) 第16章 コピーコンストラクタ
back() 関数はベクタの最終要素への参照を返す。

C++ ベクタ
[]の中にキーを書けば、そのキーをもつ要素の値にアクセスできます。mapはキーが重複登録されることがないので、このような使い方が可能です。一方、multimapの場合は、キーの重複登録が許可されているため、[]演算子ではアクセスできません。もし同じキーがあったとき、どちらのことを指しているのか判断できないからです。

なお、もし存在しないキーを指定してアクセスした場合には、そのキーを自動的に登録します。そして、そのキーに対応する値は、その値の型のデフォルトコンストラクタによって初期化されます。そのため、不正なアクセスによりエラーが発生することがないのですが、これは長所でもあり短所でもあります。むしろ、例外を発生させてくれた方が間違いが見つかりやすいという考え方もあります。

C++編(標準ライブラリ) 第10章 map

tags: c++

Posted by NI-Lab. (@nilab)