2016年2月18日木曜日

【読書ノート】Effective Modern C++ - Item 21

O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 21: 直接newを呼び出すよりもstd::make_uniqueとstd::make_sharedを使おう

std::make_sharedはC++11からあるが、std::make_uniqueはC++14から。
C++11の場合は自分で以下のコードで基本的なstd::make_uniqueを用意できる。

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

make_uniqueはパラメータをそのままperfect-forwardでコンストラクタに渡し、newでオブジェクトを作ってunique_ptrを返すだけ。
C++14からはstd名前空間に登場するから、自分で作る場合はstd以外の名前空間に置くように。

make関数には3種類あって、残りの1つはstd::allocate_shared。
第一引数に動的メモリ割り当てのためのアロケータオブジェクトを渡すことができる。

make関数を使うべき理由1: クラス名の指定の重複を避ける

auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

newを使うと2回Widgetというクラス名を指定することになり、以下の欠点が生じる。

  • ソースコードの重複はコンパイル時間の増大やコード肥大を招く可能性がある
  • 重複は一貫性の無いコードになりがちで、バグの原因になる
  • 同じものを2回タイプすることは無駄な負担である

make関数を使うべき理由2: メモリリークを防ぐ

processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

複数ある関数引数が与えられた順に実行されるとは限らない。
computePriorityが呼び出されるのはnew Widgetの前かもしれないし、std::shared_ptr生成の後かもしれないし、その「間」かもしれない
1. new Widgetが実行される
2. computePriorityが呼ばれる
3. std::shared_ptrコンストラクタが呼ばれる
もし2の段階で例外が発生した場合は3に辿りつけず、メモリリークが発生する。
以下のようにstd::make_sharedを使えば回避できる。

processWidget(std::make_shared<Widget>(), computePriority());

make関数を使うべき理由3: コードの最適化

以下のコードではnew Widgetとshared_ptrコンストラクタでそれぞれ別々にメモリアロケーションが発生する。
前者はWidgetのオブジェクトそのもの、後者はリファレンスカウントを含むコントロールブロック用のメモリアロケーションを行う。

std::shared_ptr<Widget> spw(new Widget);

一方、std::make_sharedを使用すればメモリアロケーションはWidgetオブジェクトとコントロールブロック用のメモリは1つの領域に同時に確保される。

auto spw = std::make_shared<Widget>();

そのためコンパイラで生成されるコードは短くなるし実行速度も上がる。
std::allocate_sharedも同様。

make関数を使えないケース

カスタムデリータを指定したい時

カスタムデリータを指定したい時は直接newを使うしか無い。

auto widgetDeleter = [](Widget* pw) { … };
std::unique_ptr<Widget, decltype(WidgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);

make関数を使えないのはstd::unique_ptrは上記のケースだけだが、std::shared_ptrにはもう2つ注意すべき点がある。

非常に大きなオブジェクトを扱う時

std::shared_ptrのコントロールブロックには通常のリファレンスカウントの他にもう1つのリファレンスカウントであるweak countが存在する。
これはいくつのweak_ptrがコントロールブロックを参照しているかを表す。
weak_ptrはリファレンスカウントを見てexpiredかどうかを判定する。
したがって、コントロールブロックを見ているweak_ptrが存在する限りはコントロールブロックを破棄できない。
しかしstd::make_sharedで生成されたコントロールブロックとオブジェクトは同じメモリ領域に割り当てられている。
つまりリファレンスカウントが0になってもweak countも0になる=全てのweak_ptrが破棄されるまでオブジェクト用に確保されたメモリが解放されない。
オブジェクトが消費するメモリが大きな場合は問題となり得る。
newを使った場合はリファレンスカウントが0になった時点でメモリは解放されるため、この場合はstd::make_sharedではなくnewを使った方が良いということになる。

関数引数でカスタムデリータを使う時

以下のコードはcomputePriorityが例外を発生するとメモリリークの可能性がある。

void cusDel(Widget *ptr);

processWidget(
  std::shared_ptr<Widget>(new Widget, cusDel),
  computePriority()
);

以下のようにstd::shared_ptrの生成を分離すればメモリリークは回避できるがstd::shared_ptrのコピーが発生する。
std::shared_ptrのコピーはレファレンスカウントのアトミックな操作が発生するためオーバーヘッドが無視できない。

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());

std::moveを使えばこの問題を解決できる。

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority());

2016年1月14日木曜日

【読書ノート】Effective Modern C++ - Item 18

O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Chapter4: スマートポインタ

生ポインタを愛し難い理由

  • ポインタ宣言からはそれが1つのオブジェクトを指すのか配列を指すのか分からない
  • ポインタ宣言からはそれを使い終わった時にその指し先を破棄すべきなのか分からない
  • ポインタを破棄する時にdeleteを呼べば良いのかそれとも他の破棄の仕組みがあるのか(専用の関数があるなど)が分からない
  • deleteとdelete[]のどちらを呼ぶ必要があるのか分からない。間違った選択をすると未定義動作に
  • 破棄の仕方が特定できていたとしても、1回だけ破棄を呼び出すことを全てのパス(例外発生の場合も含む)において確かにするのは難しい。もし呼び出されなかった場合はリソースがリークし、1回より多く呼び出されたら未定義動作になる
  • ポインタの指し先が既に削除されているかどうか(dangling pointerかどうか)判定する方法は無い。

生ポインタは強力なツールだが、どんなに集中してもどんなに鍛えられていてもほんの些細な過ちですぐにポインタはおかしくなってしまう。
スマートポインタがこれらの問題を解決する1つの方法であり、生ポインタよりもスマートポインタを優先して使うべきだ。

4つのスマートポインタ

  • std::auto_ptr
  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

std::auto_ptrはdeprecatedとなったC++98の異物である。
C++98にはムーブセマンティクスが無いため、std::auto_ptrはコピーした時にコピー元がnullになったりコンテナに入れられなかったりなどの問題がある。
C++11ではstd::unique_ptrはstd::auto_ptrにできることが全てできる上に実行効率も良い。
従ってstd::auto_ptrを使う唯一の場面はC++98でコンパイルする必要のある時のみであり、そうでなければ常にunique_ptrの方を使うべき。

スマートポインタのAPIは非常に多様で、唯一全てに共通するものはデフォルトコンストラクタであり、網羅的説明はしない。
網羅的説明は他所にあるから、ここではAPIの概要には書かれないようなスマートポインタを効果的に使う方法に集中する。

Item 18: 排他的な所有権を表現するリソース管理にはstd::unique_ptrを使おう

スマートポインタで最も手近に置いておくべきなのがstd::unique_ptrである。
生ポインタと同じメモリサイズであり生ポインタのほとんど全ての演算を実行でき、全く同じ命令を実行する。
従って実行速度とメモリ容量が非常に厳しい環境においてもstd::unique_ptrを生ポインタと同等に使用できる。

std::unique_ptrは排他的な所有権を持つ。
nullでないstd::unique_ptrは指し先の実体を必ず所有していて、std::unique_ptrをmoveすると所有権が移る。
排他的所有権なのでstd::unique_ptrのコピーは不可。
std::unique_ptrはmove専用の型。
std::unique_ptrのデフォルトデストラクタでは中の生ポインタにdeleteを適用する。

ファクトリ関数での利用

std::unique_ptrのありふれたユースケースはファクトリ関数。

class Investment { … };
class Stock: public Investment { … };
class Bond: public Investment { … };
class RealEstate: public Investment { … };

ファクトリ関数

template<typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params);

呼び出し

{
    auto pInvestment = makeInvestment( arguments );
    …
}

カスタムデリータ

std::unique_ptrにはコンストラクタでカスタムデリータを指定できる。
カスタムデリータは任意の関数もしくはラムダ式を含む関数オブジェクトに対応。

auto delInvmt = [](Investment* pInvestment)
                {
                  makeLogEntry(pInvestment);
                  delete pInvestment;
                }
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> // C++14ならautoで良い
makeInvestment(Ts&&... params)
{
  std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
  if (/* Stockオブジェクトを作る場合 */)
  {
    pInv.reset(new Stock(std::forward<Ts>(params)...));
  }
  else if (/* Bondオブジェクトを作る場合)
  {
    pInv.reset(new Bond(std::forward<Ts>(params)...));
  }
  …
  return pInv;
}
  • Investmentポインタ型を破棄するdelInvmtをラムダ式で生成(後述の理由でラムダ式の方が実行効率が良い)
  • カスタムデリータの型をstd::unique_ptrのテンプレート第二引数に指定
  • カスタムデリータオブジェクトをstd::unique_ptrのコンストラクタ第二引数に与える
  • std::unique_ptrに生ポインタを代入することはできない(生ポインタからスマートポインタへの暗黙の型変換は危険なため)。生ポインタの代入にはresetを用いる。
  • std::forwardを用いている理由はitem25で説明
  • カスタムデリータはInvestmentポインタを引数として取ってそれをdeleteしているので、Investmentクラスのデストラクタはvirtual指定すること

std::unique_ptrのメモリサイズ

デフォルトデリータを使う場合は生ポインタと同一サイズ。
カスタムデリータを使う場合、関数ポインタならポインタのサイズ分、関数オブジェクトならその内部状態のメモリが加わる。
関数オブジェクトがステートレス(メンバ変数を持たない)であればサイズは増加しない。
したがって、ステートレスなカスタムデリータはポインタ分サイズが増える関数よりもラムダ式を指定した方が良い。

std::unique_ptrはPimplイディオムでも使用されるが詳細はItem 22を参照。

std::unique_ptrではオブジェクトと配列を区別できる

std::unique_ptr<T>型には[]演算子は無く、std::unique_ptr<T[]>型には*と->演算子は無いなどオブジェクトのunique_ptrと配列のunique_ptrは特殊化されている。
しかしC言語ライクなAPIで配列をヒープに入れて返すようなインターフェース以外ではstd::unique_ptr<T[]>型は普通使わない。
ポインタの配列よりもstd::array, std::vector, std::stringを使用する方が良いため。

std::unique_ptrは容易にshared_ptrへ変換可能

下のコードのようにstd::unique_ptrからstd::shared_ptrへの変換は容易なので、ファクトリ関数はunique_ptrを返しておけば呼び出し側でどちらのポインタでも扱える。

std::shared_ptr<Investment> sp = makeInvestment( arguments );

std::shared_ptrの詳細についてはItem 19を参照。

2015年12月3日木曜日

【読書ノート】Effective Modern C++ - Item 14

O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 14: 例外を投げない場合はnoexceptで関数を宣言しよう

C++98における例外ととC++11における例外

コンパイラは例外の一貫性の維持を手助けしてくれないため、ある関数が投げる例外に変更が加わると呼び出し元のコードを壊す可能性がある。
開発者が関数が発生しうる例外型を記述し、変更の際は呼び出し元のコードも含めて修正を加えなければならない。
C++98において例外は苦労して使用するに値しないと考えられていた。

C++11ではnoexceptと記述することで関数が例外を出さないことを保証するようになった。
最も意味のある情報はその関数が「例外を投げるのか投げないのか」という0か1の情報だというコンセンサスが(C++11策定時に)得られたため。

noexceptをつけるかどうかはインターフェースデザインの範疇

クライアントコードにおいて関数が例外を投げるかどうかは重要な問題。
例外を投げないのにnoexceptをつけない関数は悪いインターフェースデザイン。
constと同じように例外を投げない関数には常にnoexceptを指定すべき。

コンパイラの最適化とパフォーマンスへの影響

noexceptを指定した場合は例外が発生した場合にstd::terminateでプログラムが強制終了されるようになっていて、コールスタックを管理するオーバーヘッドが削減される。
また、例外が発生して関数から出る時にオブジェクトの生成と逆順に破棄する必要もなくなり、コンパイラの最適化がかかりやすい。

RetType function(params) noexcept; // most optimizable
RetType function(params) throw(); // less optimizable
RetType function(params); // less optimizable

vectorの場合はパフォーマンスに影響する典型的な例。

std::vector<Widget> vw;
Widget w;
vw.push_back(w);

vectorはpush_backする時に容量を超えると別のメモリ領域に要素を丸々コピーしてから古いメモリ領域のオブジェクトを破棄する。
これによって強い例外保証を提供していて、コピーの途中で例外が発生してもvectorの元の状態は保存される。
C++11のムーブセマンティクスを適用する場合は元のデータを書き換えてしまうためこの強い例外保証を脅かす。
しかしmove操作がnoexceptであれば安全であり、moveを利用することでパフォーマンスが改善される。
std::vector::reserveやstd::deque::insertなど別のメモリ領域への割当が発生するメソッドについても同様のことが成り立つ。

条件付きnoexcept

noexceptには条件分岐も可能(true/falseを与えてtrueであればnoexcept)。
noexceptに関数呼び出しを与えるとnoexceptの有無がtrue/falseとして解釈されてnoexceptになるかを判定してくれる。
std::swapは以下の様な実装になっている。

template <class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));

template <class T1, class T2>
struct pair {
    …
    void swap(pair& p) noexcept(noexcept(swap(p.first, p.first)) && noexcept(swap(p.second, p.second)));
    …
};

swapはSTLの様々なアルゴリズムで使用されているため、例外が発生しないならnoexceptを指定して最適化されることが重要。

noexcept指定の注意点

noexceptを指定することは大きなメリットがあるが、もしnoexceptを指定した関数に修正を加えてnoexceptを指定しないように変更した場合はクライアントコードを壊す可能性がある。
noexceptは可能なら常に指定すべきだが、今後もnoexceptであり続けると考えられる関数に限るべき。
なお、全てのメモリ解放関数とデストラクタは暗黙の内にnoexceptとなっている。デストラクタにnoexcept(false)を明示的に指定すれば例外を発生させられるようになるが通常使うべきではない。

自然な実装で例外が発生しない関数に対してnoexcept指定すべきである。
例外を発生させないがために関数内でtry catchを書くことで複雑になってしまうようでは本末転倒。
パフォーマンス上の利点もtry catch文の追加によるオーバーヘッドでかき消されてしまう。

wide contractsとnarrow contracts

wide contracts: どのような引数を与えても未定義動作にならない
narrow contracts: 前提条件が存在し、その条件に反する引数が与えられると未定義動作になる
narrow contractsである関数では引数が不正な場合に未定義動作となってデバッグが困難となる。
そのような場合には例外で引数が不正であることを呼び出し側に伝えるのが親切。
それを分かっている設計者はwide contractsにはnoexceptをつけ、narrow contractsにはnoexceptをつけないことが多い。

関数が本当にnoexceptであるかはコンパイラがチェックしてくれるわけではない

void setup(); // functions defined elsewhere
void cleanup();
void doWork() noexcept
{
    setup(); // set up work to be done
    … // do the actual work
    cleanup(); // perform cleanup actions
}

setupもcleanupもnoexceptではないのにdoWorkはnoexcept指定されているが、コンパイルは通る。
setupとcleanupはCやC++98のライブラリかもしれない。そもそもstd::strlenなどのCの関数もnoexcept指定されていない。
このような関数が多数存在するので融通を利かせるためにコンパイラはチェックしないようにしている。

2015年10月20日火曜日

【読書ノート】Effective Modern C++ - Item 10

O'Reilly Early Releaseで購入したEffective Modern C++のドラフト版の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 10: スコープ無しenumよりもスコープ付きenumを使おう

scoped enumではスコープが有効

一般的な規則では"{"内の変数名はそのスコープ内でしか使えないが、C++98のenumはそのenumがあるスコープ内全体で使えてしまう

enum Color { black, white, red };
auto white = false; // エラー (スコープ内で既に同じ名前が宣言されているため)

C++11のscoped enumなら上述のような「リーク」が起こらない。

enum class Color { black, white, red };
auto white = false; // OK
Color c = white; // エラー (このスコープからは見えない)
Color c = Color::white; // OK
auto c = Color::white; // OK

その宣言の仕方から"enum class"とも呼ばれる。

scoped enumなら意図しない暗黙の型変換を防げる

unscoped enumは数値型に暗黙の型変換がされる。

enum Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
if (c < 14.5) { // doubleと比較
    auto factors = primeFactors(c); // intに変換
}

scoped enumは暗黙の型変換が無い。

enum class Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
if (c < 14.5) { // エラー (Colorからdoubleに変換されない)
    auto factors = primeFactors(c); // エラー (Colorからstd::size_tに変換されない)
}

scoped enumで型変換を行うにはキャストが必要。

if (static_cast<double>(c) < 14.5) {
    auto factors = primeFactors(static_cast<std::size_t>(c));
}

scoped enumは前方宣言が可能

scoped enumなら定義を記述しなくても宣言が可能

enum Color; エラー
enum class Color; // OK

C++98のunscoped enumで前方宣言ができなくなっているのは、定義を見てコンパイラがデータ型を決定しているため。 メモリを最小化するためにコンパイラがenumの定義を見て最小の型を選べるようにしていた。

enum Color { black, white, red }; // コンパイラによってはchar型を選択
enum Status { good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    indeterminate = 0xFFFFFFFF
}; // charでは表現できないのでより大きな整数型が選ばれる

前方宣言ができないならenum Statusはヘッダーファイルに書くことになる。 この時enum Statusにaudited = 500を新たに加えるとそのヘッダーファイルをインクルードしているファイルは全てリコンパイルが必要。 しかしscoped enumは前方宣言可能なので、定義を実装ファイルに分離することで無駄なリコンパイルを防げる。

scoped enumのデフォルト型はint型で、他の型にオーバーライドも可能。

enum class Status; // int型
enum class Status: std::uint32_t;
enum class Status: std::uint8_t;

もちろん定義に対しても指定可能。

enum class Status: std::uint32_t {
    good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    audited = 500,
    indeterminate = 0xFFFFFFFF
};

C++11ではunscoped enumにもこの記法が使えて、その場合は前方宣言が可能に。

enum Color: std::uint8_t; 

unscoped enumの方が便利な場面と対処法

tupleの1番目の要素を取り出したい時。

using UserInfo = std::tuple<std::string, std::string, std::size_t>;
UserInfo uInfo;
…
auto val = std::get<1>(uInfo);

unscoped enumを使えば即値の1ではなくて定数名を与えることが可能。

enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
…
auto val = std::get<uiEmail>(uInfo);

scoped enumだとくどい書き方になってしまう。

enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
…
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

ただし以下のコンパイル時に結果を返す関数を定義すれば

template<typename E>
constexpr auto toUType(E enumerator) noexcept {
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

もう少し短く書くことが可能。

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

scoped enumには今までに挙げた大きな利点があるため、この場面において少々文字を多く打つことになってもscoped enumの方を使うことに価値がある。