この記事について
C++のイディオムを紹介していきます。全12回を予定していますが私のやる気と根気でいつまで続くかわかりません。
Construct on First Use
静的オブジェクトは注意して扱わないと初期化される前に使用されることがあります。例を挙げると以下のような状況です。
#include <iostream>
class StaticObject
{
public:
StaticObject()
{
std::cout << "StaticObject::StaticObject()" << std::endl;
}
void callMethod()
{
std::cout << "StaticObject::callMethod()" << std::endl;
}
};
class MyClass
{
static StaticObject object_;
public:
MyClass()
{
std::cout << "MyClass::MyClass()" << std::endl;
object_.callMethod();
}
};
MyClass my_class; // !初期化されたないobjectにアクセス
StaticObject MyClass::object_;
int main()
{
std::cout << "main" << std::endl;
}
MyClass::MyClass()
StaticObject::callMethod()
StaticObject::StaticObject()
main
静的なオブジェクトへアクセスを関数でラップすることで、このような初期化がすんでいないオブジェクトへのアクセスを防ぐことができます。
#include <iostream>
class StaticObject
{
public:
StaticObject()
{
std::cout << "StaticObject::StaticObject()" << std::endl;
}
void callMethod()
{
std::cout << "StaticObject::callMethod()" << std::endl;
}
};
class MyClass
{
StaticObject& Object()
{
static StaticObject object_;
return object_;
}
public:
MyClass()
{
std::cout << "MyClass::MyClass()" << std::endl;
Object().callMethod();
}
};
MyClass my_class; // !初期化されたないobjectにアクセス
int main()
{
std::cout << "main" << std::endl;
}
MyClass::MyClass()
StaticObject::StaticObject()
StaticObject::callMethod()
main
Construction Tracker
コンストラクタでのメンバの初期化の際に例外が発生する場合があります。tryとcatchでくくることで例外を適切に処理できますが、ときどき困った問題が起きることがあります。
例えば以下のような場合です。
class A
{
public:
A(int n)
{
/* 初期化に失敗したら例外を投げる */
throw std::runtime_error("A Error");
}
};
class B
{
B(int n)
{
/* 初期化に失敗したら例外を投げる */
throw std::runtime_error("B Error");
}
};
class MyClass
{
A a_;
B b_;
public:
MyClass() : a_(1), b_(2)
{
}
};
int main()
{
try {
MyClass object();
} catch(...)
{
/* Aの例外?それともBの例外? */
}
}
MyClassのコンストラクタでメンバが初期化されています。初期化に失敗した際に例外を投げるのですが、例外の型が同じのためa_の初期化に失敗したのか、b_の初期化に失敗したのかわかりません。
この問題を解決するイディオムがConstruction Trackerです。
class A
{
public:
A(int n)
{
/* 初期化に失敗したら例外を投げる */
throw std::runtime_error("A Error");
}
};
class B
{
B(int n)
{
/* 初期化に失敗したら例外を投げる */
throw std::runtime_error("B Error");
}
};
class MyClass
{
A a_;
B b_;
enum TrakcerType{NONE, ONE, TWO};
public:
MyClass(TrackerType tracker=NONE)
try
: a_((tracker = ONE,1)), b_((tracker = TWO, 2))
{
/* 正常な初期化処理 */
}
catch(std::exception const& e)
{
if(tracker==ONE){
/* a_の初期化に失敗した時の処理 */
}
}
};
int main()
{
try {
MyClass object();
} catch(...)
{
/* Aの例外?それともBの例外? */
}
}
変数trackerを参照することでどのメンバ変数の初期化に失敗したかがわかります。
Copy and Swap
例外安全な代入演算処理を実装することを目的としたイディオムです。
2つのオブジェクトを入れ替えるswap関数を経由してコピーすることで
- 例外発生時に元のオブジェクトが保たれること、
- コードの重複を回避
- 自己代入の発生
この3つの問題を回避することができます。
class MyClass
{
Memory* memory_;
public:
MyClass() : memory_(new Memory)
{
}
MyClass(const MyClass& rhs)
{
/* コピーコンストラクタ */
}
~MyClass()
{
delete memory_;
}
MyClass& operator=(MyClass rhs)
{
swap(*this, rhs);
}
friend void swap(MyClass& lhs, MyClass& rhs)
{
std::swap(lhs.memory_, rhs.memory_);
return;
}
};
Copy-on-write
通称牛(COW)イディオム。オブジェクトのコピーをする際のオーバーヘッドを回避するため、遅延評価を使うことによってコピーに対して変更を加えた時のみ深いコピーを行います。
class MyClass
{
std::shared_ptr<int> data_;
public:
MyClass(): data_(new int(0))
{
}
MyClass(const MyClass& rhs)
{
shallow_copy(*this, rhs); // 浅いコピー
}
friend void shallow_copy(MyClass& lhs, const MyClass& rhs)
{
lhs.data_ = rhs.data_;
}
void modifyData(const int& num)
{
int temp = *(this->data_);
this->data_.swap(new int(temp)); // 変更があった時に深いコピーを実行する。
*(this->data_) += num;
}
};
Empty Base Optimization
下の空のクラスのサイズはどのくらいでしょうか?
class Empty {};
空っぽなので0と答える人が多いのではないでしょうか?
では、0の場合以下のプログラムはどうなるのでしょうか?
Empty empties[10];
printf("%p", &empties[2]);
サイズが0だと配列のメモリ配置が定義できなくなります。そのため、C++では空のクラスでも少なくとも1バイトのサイズを持ちます。
そのため、空のクラスを要素にもつクラス持つとクラスのメモリ上のサイズは大きくなります。
class Empty
{
public:
void emptyMethod();
};
class MyClass
{
Empty empty_; // 1バイト大きくなる。
public:
/* 省略 */
};
空のクラスをメンバにしただけなのにクラスのサイズが大きくなってしまう。なんか、嫌ですね。
そういう時は、空のクラスを継承しましょう。
継承した場合はサイズが変化しません。
class Empty
{
public:
void emptyMethod();
};
class MyClass : Empty // サイズは変化しない。
{
public:
/* 省略 */
};