読者です 読者をやめる 読者になる 読者になる

c++よく忘れること

c++

概要

  • 久しぶりにC++を書こうとすると(というよりも読もうとすると)忘れてしまっていてリハビリするのがコピーコンストラクタ,this, Template, 参照渡し,仮想関数, オーバライド,スマートポインタ.

コピーコンストラクタと代入演算子オーバーロード

  • C++では関数にオブジェクトを値渡しで渡す場合,宣言時の初期化,返り値とした場合にコピーコンストラクタが呼ばれる.
  • 値渡しじゃなく,参照渡し(リファレンス&,ポインタ*)の場合はコピーコンストラクタは呼ばれない(コピーしないんだから).
  • 変数宣言時の初期化のときの"="はコピーコンストラクタ呼ばれる(だって「初期化」だから)けれど,それ以外の代入のとき代入演算子=が呼ばれる.だから,自前クラスに=を使う場合はコピーコンストラクタの定義と代入演算子オーバーロードが必要.
  • 返り値にした場合もコピーコンストラクタが呼ばれるのは忘れやすいので注意.
  • わざわざあるコピーコンストラクタが存在する理由は,ポインタを持っているオブジェクトをコピーされると,そのコピーされたオブジェクトがスコープを抜ける時にデストラクタが呼ばれて,もとのオブジェクトのポインタを解放してしまうから.例えば,クラスの中でメモリ参照をしている(newしている)ような場合,値渡しで単純にコピーするとそのポインタがコピーされる.そして,そのオブジェクトが関数を抜ける時にデストラクタが呼ばれてしまうと,もともとの参照していたポインタも解放されてしまう.(関数呼び出し元で再度デストラクタが呼ばれると2重解放になってエラー発生して止まる)
  • 基本的には代入演算子はオーバロードして,コピーコンストラクタを書くようにする.ポインタをメンバ変数に持っている場合は必須.
#include <iostream>
#include <string>

using namespace std;

class TestClass {
public:
  TestClass(){ // Default constructor
    cout << "Default constructor is called." << endl;
  }

  TestClass(const string name) : name_(name){ // Custom constructor
    cout << "Custom constructor is called, I am name=." << this->name_ << endl;
  }

  TestClass(const TestClass & c){ // Copy constructor
    name_ = c.name_ + "_copy";
    cout << "Copy constructor is calld, I am name=." << this->name_ << endl;
  }

  string getName() {return name_;}
private:
  string name_;
};
void value_pass_func(const TestClass c) {
  return;
}

void ref_pass_func(const TestClass & c) {
  return;
}

void pointer_pass_func(const TestClass * c) {
  return;
}

int main(void) {
  TestClass c0;          // default constructor is called.
  TestClass c1 {"hoge"}; // custom constructor is called.
  TestClass c2 = c1;     // copy constructor is called.

  cout << "value_pass_func is called," << endl;
  value_pass_func(c2);
  cout << "ref_pass_func is called," << endl;
  ref_pass_func(c2);
  cout << "pointer_pass_func is called," << endl;
  pointer_pass_func(&c2);
  return 0;
}

this

  • メソッド呼び出しの際には暗黙で呼び出し元のオブジェクトのポインタが渡される.それがthis.pythonで言うところのselfなのかな.
  • メンバ関数を普通に使用する分には省略する.
  • 使いみちとしては,メンバ関数ではない関数(クラスの外にある関数)にオブジェクトを渡す時に使う.

Template

  • 型が違うだけで他は同じクラス,関数を一つにまとめる.具体的な型は呼び出し側で指定して,コンパイラコンパイル時に型を埋め込む.(当然だけど動的言語みたいに実行時に型を解決している訳じゃない.)
  • 同様のことは継承(多態性)でも出来る.違いはtemplateはコンパイル時に型を解決しているのに対して,継承は実行時にvtable(virtualな関数のテーブルをオブジェクトは持っている)を手繰って実行するべき関数を検索している.よって,継承のほうが実行コストが高い.templateは型付きの関数をコンパイル時に自動生成しているようなもの.但し,templateはコンパイル時に型を特定する必要があるが故に,次の通りヘッダに実装を書く必要があって汚くみえる.
  • テンプレートを使う場合はクラスの実装をヘッダファイルに記述する.これは分割コンパイルはあくまでもソース単位で実行されるため,テンプレートの実装”だけ”を書いたソースをコンパイルしても,それが使われていなので実態が生成されないため.よって,ヘッダにしておいて,そのテンプレート関数なりクラスを使うソース(は当然それをインクルードしていて,型を指定する記述が登場する)に渡すのだ. (補足として,Cではヘッダファイルのインクルードは極端に言えばただの置換で,インクルードしたファイルの内容がそこに書き込まれると思えば良い.ソースファイルのコンパルはその置換後のファイルに対して実行される.これがヘッダファイルだけのライブラリが使われる理由.つまり,外部ファイルとかなく,ヘッダだけで真に完結する.コンパイル時間が大きくなるけどプリコンパイルを使うと避けられる.)
  • テンプレートのスコープはtemplate文の次の宣言文の中だけ.
#include <iostream>
#include <string>

using namespace std;

// この関数が型Xを使うことを宣言
template <class X>
X add(X a, X b)
{
  return a+b;
}

int main()
{
  cout << add<int>(1, 2) << endl;
  cout << add<float>(1.0, 2.0) << endl;
  cout << add<string>("Oda", "Nobunaga") << endl;
  return 0;
}

オーバーライド

  • 親クラスのメンバ関数を子クラスが上書きすることができる.
  • virtualを付けなくてもできることに注意.じゃあvirtualの意味は?というのは下を参照.
  • 演算子の再定義や,引数の異なる関数を定義することはオーバロード(多重定義).

virtual

  • C++では子クラスは親クラスのメソッドと同名のメソッドを定義可能.
  • virtualと書くのは基底側だけでも良いけど,分かりやすいように継承側でも書く.
  • virtualを付与すると実行時に型を解決して,どの関数を実行するのかを検索するので,多少はオーバヘッドがある.
  • 親クラスでは中身が無い,つまり子クラス側で実装をお願いする関数を純粋仮想関数と呼び,純粋仮想関数を持つクラスを抽象クラスと呼ぶ.抽象クラスは実態を作れない.
class Hoge
{
public:
  virtual void func(void) = 0; // 純粋仮想関数
};
  • virtualを付ける場合と付けない場合の違いは,ポインタ経由でメソッドを呼び出す時に現れる.C++では親クラスのポインタに子クラスのアドレスを入れることができて,その時に(おそらく)意図と異なる動作になってしまう.つまり,親クラスのポインタに子クラスのアドレスを入れて,親と子で同名のメソッドをポインタ経由で呼び出した時にどうなるか.virtualが付いていれば子クラスのメソッドが呼ばれて,付いていなければ親クラスのメソッドが呼ばれる.
#include <iostream>

using namespace std;

class Hoge {
public:
  void non_virtual_func (void) {cout << "I am Hoge (non_virtual)" << endl;};
  virtual void virtual_func (void) {cout << "I am Hoge (virtual)" << endl;};  
};

class Foo : public Hoge {
public:
  void non_virtual_func (void) {cout << "I am Foo (non_virtual)" << endl;};
  virtual void virtual_func (void) {cout << "I am Foo (virtual)" << endl;};
};


int main()
{
  Hoge *h1 = new Hoge();
  h1->non_virtual_func(); // I am Hoge (non_virtual)
  h1->virtual_func();     // I am Hoge (virtual)

  Hoge *h2 = new Foo();
  h2->non_virtual_func(); // I am Hoge (non_virtual) !!
  h2->virtual_func();     // I am Foo (virtual)

  return 0;
}

関数オブジェクト(ファンクタ)

  • Cでは関数ポインタを使って関数に関数を渡せた.同じような目的で使えるのが関数をオブジェクト化した関数オブジェクト.
  • テンプレートと組合せて使われる例が多いけれど,それは(おそらく)STLVectorとかに渡す場合を想定しているから.ファンクタ自体はテンプレートと関係は無い.
  • 実装はoperator()をオーバーロードする.
#include <iostream>

using namespace std;

template <typename T>
class MyAdder {
public:
  T operator() (const T& a, const T& b) { // functor
    return a+b;
  }
};

int main(void) 
{
  MyAdder<double> double_adder;
  MyAdder<int> int_adder;
  cout << double_adder(2.0, 1.2) << endl;
  cout << int_adder(2, 1) << endl;
  cout << int_adder(2.3, 1) << endl; // Warning
}

スマートポインタ

  • new/deleteの面倒さと危険さを回避するために導入された.
  • auto_ptrは使うな,unique_ptrを使え.
  • unique_ptrはスコープからでると自動で削除される.shared_ptrは参照数をカウントしていて,参照が0になったら削除される.

    unique_ptr

  • C++14以降はmake_uniqueで見た目がすっきりする.
#include <memory>
std::unique_ptr<Class_name> p(new Class_name(arg*)); // c++14以前
std::unique_ptr<Class_name[]> p(new Class_name[N]); // N個の配列として渡す.
auto v = std::make_uniqe<Class_name>(arg*);
std::unique_ptr<Class_name[]> v_arr = std::make_uniqe<Class_name>(size); // 配列でかつ,引数渡したいときは?

vector

  • 初期化方法と,emplace_back()とpush_back()の違いを忘れてしまう.emplace_back()はクラスのオブジェクトをvectorに入れる場合に使う.コンストラクタの引数を渡したいというのと,コピーコンストラクタのコストを下げるため.
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

using namespace std;


int main()
{
  //////////////////////////////////////////////////
  // Construction
  //  - initialize with size and init. value
  vector<int> v0(10, 0); // {0,0,0,0...0}

  //  - initialize with a vector
  vector<int> v1(v0);

  //  - initializer list (from c++11)
  vector<int> v2{0,1,2,3,4,5,6,7,8,9};

  //  - initialize with an array
  int arr[] = { 100, 600, 300, 500, 400, 200 };
  vector<int> v3(arr, arr + (sizeof(arr)/sizeof(int)));

  //  - initialize with iota
  vector<int> v4(10);
  iota(v4.begin(), v4.end(), 5); // {5,6,7,...14}
  
  //////////////////////////////////////////////////
  // Add element
  //  - If add element is an object, use emplace_back()
  //    which can pass the constructer args.
  v0.push_back(10); // v0={0,0,0...0,10}
  // vobj.emplace_back(arg1, arg2,...)

  //////////////////////////////////////////////////
  // Algorithm
  //sort(viter, vend);
  

  // ranged for
  for (auto d : v4) 
    cout << d << endl;

  return 0;
}