effective modern c++勉強会#4 item 17, 18資料
TRANSCRIPT
Effective Modern C++勉強会#4
Item 17,18東京大学物性研究所 特任研究員
五十嵐 亮(@rigarash)
この資料は http://1drv.ms/1DLhxPS にあります。
Item17:特殊メンバ関数の生成について理解しよう1: 概要
特殊メンバ関数(special member function)とは?
C++で勝手に(暗黙に)生成されても構わない関数
C++98では4つのみ
デフォルトコンストラクタ
デストラクタ
コピーコンストラクタ
コピー代入演算子
C++11で2つ追加
ムーブコンストラクタ
ムーブ代入演算子
Item 17:特殊メンバ関数の生成について理解しよう2: C++98での注意点
必要になったときにのみ生成される
暗黙にpublicでinlineでnonvirtual(例外あり)
基底クラスのデストラクタがvirtualならvirtual
デフォルトコンストラクタ
クラスにコンストラクタが一つも定義されてないときのみ生成される
引数のあるコンストラクタを定義したときには生成されない
Item 17:特殊メンバ関数の生成について理解しよう3: C++11で追加された関数
ムーブコンストラクタとムーブ代入演算子
生成ルールと振る舞いはコピーと同等
必要となった時に生成
非静的(non-static)メンバは個別にムーブ(std::move)
基底クラスの非静的メンバも(あれば)ムーブ生成・代入
class Widget { public:...Widget(Widget&& rhs); // ムーブコンストラクタWidget& operator=(Widget&& rhs); // ムーブ代入演算子
...}
ムーブ操作(ムーブコンストラクタとムーブ代入演算子)
ムーブが実際に行われる保証はない
ムーブの要請
ムーブ操作のサポートがなければコピーを利用する
関数オーバーロードで決定(std::move()を使うため)
cf. Item 23
Item 17:特殊メンバ関数の生成について理解しよう4: ムーブ操作
コピーコンストラクタとコピー代入演算子
互いに独立(どちらかのみ宣言してももう一方は自動生成)
後方互換性のためこの挙動は残る
ムーブ操作の宣言で自動生成は抑制
一見C++98より制限が厳しくなったようにみえるが、C++98にはムーブ操作は存在しないため問題ない
ムーブコンストラクタとムーブ代入演算子
互いに独立ではない
どちらかを宣言すると、もう片方の自動生成は抑制
コピー操作、デストラクタの宣言で自動生成は抑制
Item 17:特殊メンバ関数の生成について理解しよう5: ムーブ操作とコピー操作の関係
「コピーコンストラクタ、コピー代入演算子、デストラクタのどれかを宣言するなら、3つとも宣言するべき」
C++98からの有名なガイドライン
コピー操作の必要性はリソース管理に由来する
1. あるコピー操作でのリソース管理はもう一方でも必要
2. デストラクタはリソース管理(資源の開放)を行う
特にリソース管理としての「メモリ」
メモリ管理を行う標準ライブラリはすべて3つ宣言
動的にメモリを管理するSTLコンテナなど
Item 17:特殊メンバ関数の生成について理解しよう
6: Rule of Three
自動生成のコピー操作が正しいときに明示的に指定する
多相的な基底クラスで有用
一般に仮想デストラクタを持つ
基底クラスのポインタを介したdelete, typeidなどが未定義
自動生成のコピー操作の振る舞いは正しいことが多い
Item 17:特殊メンバ関数の生成について理解しよう
7: =default
class Widget {public:...~Widget(); // ユーザ定義デストラクタ
...Widget(const Widget&) = default; Widget& operator=(const Widget&) = default; …
};
仮想デストラクタでもOK
Item 17:特殊メンバ関数の生成について理解しよう
8: =default 2
class Base { public:virtual ~Base() = default; // デストラクタをvirtualに
Base(Base&&) = default; // ムーブをサポート
Base& operator=(Base&&) = default;
Base(const Base&) = default; // コピーをサポート
Base& operator=(const Base&) = default;
...};
StringTableの宣言
次のように変えると…
Item 17:特殊メンバ関数の生成について理解しよう9: StringTableの例
class StringTable { public:
StringTable() {} ... // copy/move/dtor以外の関数
// 例えばinsert, erase, lookup
private:std::map<int, std::string> values;
};
StringTableの例
ムーブ操作が自動生成されない!
std::mapのコピーはムーブよりとても遅い
明示的に=defaultを書く習慣をつけてもよい
Item 17:特殊メンバ関数の生成について理解しよう10: StringTableの例 2
class StringTable { public:
StringTable() { makeLogEntry("Creating StringTable object"); } // 追加
~StringTable(){ makeLogEntry("Destroying StringTable object"); } // 追加... // move以外の関数
// 例えばinsert, erase, lookup
private:std::map<int, std::string> values;
};
ユーザー関数テンプレートは自動生成規則に影響しない
次の例ではTがWidgetでも自動生成される
Item 17:特殊メンバ関数の生成について理解しよう11: ユーザー関数テンプレート
class Widget { template<typename T> Widget(const T& rhs); template<typename T> Widget& operator=(const T& rhs);
};
デフォルトコンストラクタ
C++98と同じ
デストラクタ
C++98とほぼ同じ
デフォルトでnoexceptになった
Item 17:特殊メンバ関数の生成について理解しようC++11ルールのまとめ
コピーコンストラクタ
実行時の振る舞いはC++98と同じ
ユーザー定義コピー代入演算子かデストラクタの存在時の自動生成に依存するコードは推奨されない
コピー代入演算子
実行時の振る舞いはC++98と同じ
ユーザー定義コピーコンストラクタかデストラクタの存在時の自動生成に依存するコードは推奨されない
Item 17:特殊メンバ関数の生成について理解しようC++11ルールのまとめ
ムーブコンストラクタとムーブ代入演算子
非静的メンバのメンバ別ムーブ
コピー操作、ムーブ操作、デストラクタがない場合のみ生成される
Item 17:特殊メンバ関数の生成について理解しようC++11ルールのまとめ
特殊メンバ関数はそれ自身の明示的な宣言がないときコンパイラが暗黙に生成するもの
デフォルトコンストラクタ
デストラクタ
コピー操作(コピーコンストラクタとコピー代入演算子)
ムーブ操作(ムーブコンストラクタとムーブ代入演算子)
ムーブ操作はコピー操作、デストラクタが明示的に宣言されてないときのみ生成される
コピーコンストラクタはムーブ操作が宣言されるとdeleteされる
デストラクタの明示的な宣言があるときのコピー操作の生成は推奨されない
メンバ関数テンプレートは特殊メンバ関数の生成を止めない
Item 17:特殊メンバ関数の生成について理解しよう
Things to Remember
スマートポインタ
生ポインタの問題点
1. 指す先がオブジェクトか配列かわからない
2. 使い終わったあと破棄すべきかわからない
3. どのように破棄すべきかわからない
deleteや破棄専用関数
4. deleteだとして、deleteとdelete[]のどちらかわからない
5. 一度だけ確実に破棄するのが難しい
例外を含めて多数のコードパスがある
6. ダングリングポインタかどうかわからない
破棄されたけオブジェクトを指し示すポインタ
スマートポインタ
スマートポインタとは
生ポインタのラッパーで、落とし穴を避けるためのもの
C++11でのスマートポインタ
std::auto_ptr (C++98, 非推奨)
C++98のコンパイラを使う必要がない限り使わないこと
std::unique_ptr (C++11, Item 18)
std::shared_ptr (C++11, Item 19)
std::weak_ptr (C++11, Item 20)
APIの解説ではなくスマートポインタを効果的に使うための情報のまとめ
std::unique_ptr
生ポインタと同じサイズ、(ほとんどの場合)同じ命令
メモリとCPU資源に乏しいときにもOK
排他的な所有権(exclusive ownership)を表現
std::unique_ptrがnon-null
所有権を有する
Item 18:専有リソースにはstd::unique_ptrを使おう
std::unique_ptrの移動
所有権の移動(移動元はnullに)
std::unique_ptrのコピー
禁止(排他的な所有権の複製はできない)
std::unique_ptrはmove-onlyな型
std::unique_ptrのデストラクト
所有権のあるリソースの破棄
所有する生ポインタをdelete
Item 18:専有リソースにはstd::unique_ptrを使おう
クラス階層があるオブジェクトをファクトリ関数へ
次のような関数から利用
利用方法
Investme
nt
Bo
nd
Sto
ck
RealEs
tate
template<typename... Ts> // return std::unique_ptrstd::unique_ptr<Investment> // to an object createdmakeInvestment(Ts&&... params); // from the given args
{ ...auto pInvestment = // pInvestmentは
makeInvestment( arguments ); // std::unique_ptr<Investment>...
} // *pInvestmentをデストラクト
Item 18:専有リソースにはstd::unique_ptrを使おう典型的な利用例
所有権の移譲シナリオでも利用できる
vec.push_back(makeInvestment( arg ));
コンテナが破棄されるときにunique_ptrも破棄される
以下の場合を除き例外が飛んだときでも破棄される
例外がエントリポイントの外まで伝播する
noexceptの仕様に反する
std::abortやstd::exitなどの終了関数が呼ばれる
Item 18:専有リソースにはstd::unique_ptrを使おう典型的な利用例
デフォルトではdeleteを呼ぶ
任意の関数、あるいは関数オブジェクト(ラムダ式含む)
もカスタムデリータにすることができる
破壊時にログを出力する例
Item 18:専有リソースにはstd::unique_ptrを使おうカスタムデリータ
// ラムダ式のカスタムデリータ
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment); delete pInvestment;
};
template<typename... Ts> std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);...
}
ラムダ式でのカスタムデリータ
関数よりも効率がよい
カスタムデリータの型をテンプレートの第2引数に
decltype(Item 3)を利用するとよい
カスタムデリータをコンストラクタの第2引数に
生ポインタを代入できない
代わりにpInv.reset()を使う
引数を渡すにはstd::forward(Item 25)を使う
Item 18:専有リソースにはstd::unique_ptrを使おうカスタムデリータ
if ( ... ){
pInv.reset(new Stock(std::forward<Ts>(params)...)); }
function return type deduction(Item 3)がある
Item 18:専有リソースにはstd::unique_ptrを使おう
C++14
template<typename... Ts> auto makeInvestment(Ts&&... params) { // ラムダ式のカスタムデリータ; 関数内に定義
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment); delete pInvestment;
};
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
...}
デフォルトのデリータ(delete)なら生ポインタと同じ
関数ポインタの場合
1-2 word分増加
関数オブジェクトの場合
ステートレス(キャプチャなしラムダ式など)ならペナルティなし
Item 18:専有リソースにはstd::unique_ptrを使おうカスタムデリータのメモリ使用量
キャプチャなしラムダ式なら必要ないコストがかかる
void delInvmt(Investment* pInvestment) {
makeLogEntry(pInvestment); delete pInvestment;
}
// Investment* と関数ポインタが必要
template<typename... Ts> std::unique_ptr<Investment,
void (*)(Investment*)>makeInvestment(Ts&&... params);
Item 18:専有リソースにはstd::unique_ptrを使おうカスタムデリータのメモリ使用量
Pimplイディオム(Item 22)でも利用される
配列用にstd::unique_ptr<T[]>もある
operator[]はあるがoperator*, operator->はない
C APIのようなものを使うとき以外はarray, vector, stringがベター
std::shared_ptrに代入して変換できる
Factory関数の返り値にはshared_ptrよりもよい
Item 18:専有リソースにはstd::unique_ptrを使おうその他
std::unique_ptrは以下の特徴を持つスマートポインタ
小さい
速い
ムーブ専用
専有リソースの管理
デフォルトのデリータはdeleteだが、カスタムも可能
ステートフルなデリータと関数ポインタはサイズ増加
std::unique_ptrはstd::shared_ptrに簡単に変換できる
Item 18:専有リソースにはstd::unique_ptrを使おう
Things to Remember