class: center, middle # .title[Effective C++ 総集編] ## hatsusato KMC春合宿2017 --- # Self-introduction ![enter](enter.png) ## @hatsusato - KMC5回生 まもなくM2 - 理学部数学系卒 - 五十嵐研究室 - プログラミング言語つくる - 研究の進捗は順調ですが、コンパイラの進捗はダメです --- layout: true # Introduction --- name: introduction - C++は *"expert friendly"*[1] とも呼ばれるように、習得の難しい言語です。 - 実際、初心者が入門書を読んだだけでは、堅牢で効率のよいコード(正しいコード)をC++で書くことは非常に困難です。 - このようなC++の学習をとりまく状況の中で、 *Effective C++* は中級以上へとステップアップするために欠かせない本として不動の地位を築いてきた。 .footnote[[1] by Bjarne Stroustrup, [The Problem with Programming](https://www.technologyreview.com/s/406923/the-problem-with-programming/)] --- - 一方、時間の流れの中で *Effective C++* は着実に古びていっている。 - 現在手に入る日本語の第3版の初刷は2006年のことであり、C++11以後の現在とは隔世の感がある。 - 確かに *Effective C++* には現在にもなお通じる重要なアドバイスが豊富にまとまった良書ではあるが、C++11以後では推奨されなくなっていたり、誤っていたりする主張も含まれていて、現在ではおすすめしづらい本へと変わってきている。 - C++11対応を謳った *Effective Modern C++* は、結局 *Effective C++* とは別の内容を扱っており、著者もどうやらこれ以上 *Effective C++* を更新するつもりはないらしい[2]。 .footnote[[2] http://scottmeyers.blogspot.jp/2015/12/good-to-go.html] --- - 以上を踏まえて、*Effective C++* の古い内容を整理して、重要な情報のみを凝縮したドキュメントが必要だという結論に至りました。 - 古い内容を改める部分などは、C++11以降の状況や個人の考えも入ったものになっているので、元の本から大きく異なる部分もあります。ご了承ください。 --- - *Effective C++* 全体の内容の分量は発表するには多すぎるので、今回はその一部を発表するつもりです。 - 本来は副読本として *Effective C++* と合わせて読んだり、勉強会などでドキュメントとして活用してもらうことを想定しています。 ??? 内容としてはあまり春合宿向きではないが、春合宿の発表をすることになれば、スライドを作らざるをえないので、この発表をすることにした。 --- name: toc-header layout: true # [Table of contents](#table-of-contents) --- template: toc-header name: table-of-contents layout: false - [第1章 C++に慣れよう](#chapter1) - [第2章 コンストラクタ、デストラクタ、コピー代入演算子](#chapter2) - [第3章 リソース管理](#chapter3) - [第4章 デザインと宣言](#chapter4) - [第5章 実装](#chapter5) - [第6章 継承とオブジェクト指向設計](#chapter6) - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 - [まとめ](#conclusion) --- template: toc-header name: chapter1 - **第1章 C++に慣れよう** - 第2章 コンストラクタ、デストラクタ、コピー代入演算子 - 第3章 リソース管理 - 第4章 デザインと宣言 - 第5章 実装 - 第6章 継承とオブジェクト指向設計 - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- layout: true ## 1項 C++を複数の言語の連合とみなそう (p.1) --- name: item-1 - C++はマルチパラダイム言語。 - 4つの言語からなると考えるとわかりやすい。 - それぞれで書き方(規約)が異なるので分けて考えるとよい。 --- - C ```C++ int min(int x, int y) { if (x < y) { return x; } else { return y; } } ``` --- - オブジェクト指向C++ ```C++ class Base { virtual ~Base() = default; virtual void f() = 0; }; class Derived : public Base { void f() override {} }; ``` --- - テンプレートC++ ```C++ template
typename std::enable_if_t< std::is_constructible_v
, std::unique_ptr
> make_unique(Args&&... args) { return std::unique_ptr
( new T(std::forward
(args)...)); } template
typename std::enable_if_t< !std::is_constructible_v
, std::unique_ptr
> make_unique(Args&&... args) { return std::unique_ptr
( new T{std::forward
(args)...}); } ``` --- - STL ```C++ int main() { std::vector
v; const auto rand = [](){ static std::random_device rd; static std::mt19937 gen(rd()); static std::uniform_int_distribution<> dist(0, 9); return dist(gen); }; std::generate_n(std::back_inserter(v), 10, rand); std::copy(begin(v), end(v), std::ostream_iterator
(std::cout, " ")); std::cout << std::endl; } ``` --- layout: true ## 2項 `#define` より、`const`、`enum`、`inline`を使おう (p.3) --- name: item-2 - 今やC言語の方でも成り立つ指針になっていますが、定数やインライン関数を表すのに**マクロを使うのをやめましょう**。 - *`enum`を使おう* というのは`enum`ハックの話で、もはや現代的ではありません。`static const`を用いましょう。 ```C++ static const double PI = std::acos(-1.0); template
inline T& min(T& x, T& y) { (x < y) ? x : y; } ``` --- - `inline`に関しては、**現代のコンパイラは人間より賢い**です。`inline`指定ではなく最適化オプションを用いましょう。 - 最適化オプションを使えば、**たとえ`inline`指定していなくても**、インライン化すべきとコンパイラが判断すればインライン化されます。 - テンプレート関数などとは違って、ソースファイルに書かれたコードであっても、リンク時最適化を行えばインラインにできます。 - **早すぎる最適化は悪**です。ベンチマークを取ってからインライン化について考えましょう。 --- layout: true ## 3項 可能ならいつでも`const`を使おう (p.8) --- name: item-3 次の違いを指摘できますか? ```C++ char greeting[] = "Hello"; char *p0 = greeting; char const *p1 = greeting; const char *p2 = greeting; char *const p3 = greeting; char const *const p4 = greeting; const char *const p5 = greeting; ``` -- - `*`の**左**に`const`があれば、ポインタの参照先の領域が`const`になる。 `"Hello"`を書き換えられない。 - `*`の**右**に`const`があれば、ポインタ変数自身の領域が`const`になる。 別のオブジェクトを指すようにしたり、イテレートしたりできない。 - `const`が`char`の右か左かは関係ない。 --- - 可能な限り、ローカル変数などに**`const`指定しましょう**。 - 値渡しや戻り値への`const`指定もできますが、あまり意味がないです。 - 可能な限り、`iterator`の代わりに`const_iterator`を使いましょう。 - 可能な限り、メンバ関数に`const`指定をつけましょう。 - `const`版と非`const`版のオーバーロードを提供すべき場面もあります。 --- ラムダ式を使えば、どこでも`const`初期化できる。 ```C++ const auto line = [](){ std::string buf; std::getline(std::cin, buf); return buf; }(); ``` --- - メンバ関数への`const`指定は、あくまでもそのクラスのメンバ変数の*ビットレベル不変性*を保証するものでしかない。 - つまり `T *const` として扱うということであって、 `const T*` という意味ではない。 - よって、ポインタの指す先ならば**`const`メンバ関数でも書き換えられる**ので注意。 --- - クラスの*論理的な不変性*を保証するための機能として`mutable`というキーワードがありますが、スレッドセーフを勉強してから使うようにしてください。 - cf. *Effective Modern C++ 項目16 `const`メンバ関数はスレッドセーフにする* - ラムダ式に指定できる`mutable`は、全くの別物なので、スレッドセーフを気にする必要はありません。 - 単にラムダ式の関数呼び出しオーバーロードに暗黙についている`const`指定を剥がすだけのものです。 ```C++ int x = 0; auto f = [x]() mutable { x = 1; return x; }; ``` --- - 以下は`operator[]`オーバーロードなどの実装でしばしば用いられるイディオムです。 - アクセサ関数の提供という限られた場面でしか使えないですが、知っておくとよいです。 ```C++ class string { public: // ... const char& operator[](std::size_t pos) const { return str_[pos]; } char& operator[](std::size_t pos) { return const_cast
( * static_cast
(*this)[pos] ); } // ... private: char *str_; }; ``` --- layout: true ## 4項 オブジェクトは、使う前に初期化しよう (p.18) --- name: item-4 - 変数の定義時には**必ず初期化式を与えましょう**。 - 初期化と代入は異なります。 - Cとは違い、C++では任意の位置でオブジェクトを定義できます。 - クラスのコンストラクタでは、すべてのメンバを適切に初期化しましょう。 --- - コンストラクタでは*メンバ初期化リスト*という初期化構文を用いましょう。 - 初期化の順序はメンバ初期化リストの順ではなく、クラス内のメンバ変数の宣言順です。 - メンバ初期化リストの順番はメンバ変数の宣言順と合わせましょう。 ```C++ struct X { X() : a(0), b(1.0), c('2') {} int a; double b; char c; }; ``` --- - グローバルな静的オブジェクトは、本質的に初期化順序の問題を抱えており、**解決は不可能**です。 - C++は異なる翻訳単位における静的オブジェクトの初期化順序を定めていません。 - グローバルな静的オブジェクトを使うのをやめて、代わりに**ローカルな静的オブジェクト**を使うようにしましょう。 - これで初期化の順序を明示的に制御できるようになりますが、破棄の順序は同じく不定なので、デストラクタで順序に依存する処理を行わないように注意しましょう。 ```C++ Singleton& static_object() { static Singleton object; return object; } ``` --- template: toc-header name: chapter2 - 第1章 C++に慣れよう - **第2章 コンストラクタ、デストラクタ、コピー代入演算子** - 第3章 リソース管理 - 第4章 デザインと宣言 - 第5章 実装 - 第6章 継承とオブジェクト指向設計 - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- layout: true ## 5項 C++が自動で書き、自動で呼び出す関数を知ろう (p.27) --- name: item-5 - 以下のメンバ関数を*特殊メンバ関数*と呼ぶ。 - デフォルトコンストラクタ - デストラクタ - コピーコンストラクタ - コピー代入演算子 - ムーブコンストラクタ - ムーブ代入演算子 - 特殊メンバ関数は、宣言していなくても、特定の条件を満たせば暗黙に生成されます。 - cf. *Effective Modern C++ 項目17 自動的に生成される特殊メンバ関数を理解する* --- - コンストラクタが1つも宣言されていないとき、デフォルトコンストラクタは暗黙生成されます。 - ここでのコンストラクタには、コピーコンストラクタやムーブコンストラクタを含みます。 - それ以外の特殊メンバ関数が暗黙に生成される条件は、それ自体を覚えても解決とはなりません。 次の3つの**ルール**を覚えておきましょう。 - *Rule of three* - *Rule of five* - *Rule of zero* --- *Rule of three* - もし、デストラクタ・コピーコンストラクタ・コピー代入演算子のうち、1つでも宣言する必要があるのならば、この**3つすべて**を定義する必要がある。 - このとき、ムーブコンストラクタ・ムーブ代入演算子の暗黙生成は抑制される。 *Rule of five* - もし、ムーブコンストラクタ・ムーブ代入演算子のうち、1つでも宣言する必要があるのならば、デストラクタ・コピーコンストラクタ・コピー代入演算子を含めた**5つすべて**を定義する必要がある。 --- *Rule of zero* - デフォルトコンストラクタを除く特殊メンバ関数は、極力**宣言しない**ようにする。 - 通常は特殊な管理の必要なクラスを作る必要はなく、そういったクラスを組み合わせるだけでよいはず。 - 特殊な管理をするクラスは一つのものごとだけを管理する(*Single Responsibility Principle*)。 --- - 暗黙に生成されるものと同じ関数定義を与えたいときは、特殊メンバ関数の定義を明示的に`default`生成させることができる。 ```C++ struct Empty { Empty() = default; ~Empty() = default; Empty(const Empty&) = default; Empty& operator=(const Empty&) = default; Empty(Empty&&) = default; Empty& operator=(Empty&&) = default; }; ``` --- layout: true ## 6項 コンパイラが自動生成することを望まない関数は、使用を禁止しよう (p.31) --- name: item-6 - 特殊メンバ関数に`delete`を指定すると、その関数の生成を明示的に禁止できる。 - 以下はコピーできないクラスの例です。 ```C++ template
struct Uncopyable { protected: Uncopyable() = default; ~Uncopyable() = default; * Uncopyable(const Uncopyable&) = delete; * Uncopyable& operator=(const Uncopyable&) = delete; Uncopyable(Uncopyable&&) = default; Uncopyable& operator=(Uncopyable&&) = default; }; ``` --- - 特殊メンバ関数に`delete`を指定すると、その関数の生成を明示的に禁止できる。 - 以下はムーブできないクラスの例です。 ```C++ template
struct Unmovable { protected: Unmovable() = default; ~Unmovable() = default; Unmovable(const Unmovable&) = default; Unmovable& operator=(const Unmovable&) = default; * Unmovable(Unmovable&&) = delete; * Unmovable& operator=(Unmovable&&) = delete; }; ``` --- - 一般に、コピーやムーブを禁止したいクラスを作るときには、次のイディオムがしばしば用いられる。 - *Rule of zero* を思い出そう。 ```C++ class YourClass : private Uncopyable
, private Unmovable
{ // YourClass can neither copy nor move }; ``` - 先ほどの`Uncopyable`や`Unmovable`の実装において、テンプレート(CRTP)や`protected`が使われていたのは、このイディオムのように`private`継承での使い方を想定しているからです。 - *空の基底クラスの最適化*を利用し、*菱型継承問題*に対処している。 - cf. [39項 `private`継承は賢く使おう](#item-39) --- - 一般のメンバ関数でも`delete`を指定することで、その関数を呼び出せないようにすることができる。 ```C++ #include
template
void print(const T& x) { std::cout << x << std::endl; } template
void print(T* x) = delete; ``` --- layout: true ## 7項 ポリモーフィズムのための基底クラスには仮想デストラクタを宣言しよう (p.34) --- name: item-7 - 動的ポリモーフィズムを利用したいときは、基底クラスのデストラクタを`virtual`にしなければなりません。 ```C++ class Base { public: // ... * ~Base() = default; virtual void f() = 0; }; class VBase { public: // ... * virtual ~VBase() = default; virtual void f() = 0; }; ``` ??? デストラクタを明示的に宣言しているので、当然他の特殊メンバ関数も定義する必要があります。 --- - 動的ポリモーフィズムを利用したいときは、基底クラスのデストラクタを`virtual`にしなければなりません。 ```C++ class Derived : public Base { public: void f() override {} }; class VDerived : public VBase { public: void f() override {} }; ``` --- - さもないと、派生クラスのオブジェクトを基底クラスの型として破棄したときに、派生クラスのデストラクタが呼び出されず、**リソースリーク**につながります。 - デストラクタが仮想関数でないと、動的なオブジェクトの型を探索できないので、静的な型のデストラクタが呼び出されてしまう。 ```C++ #include
int main() { std::unique_ptr
b = std::make_unique
(); std::unique_ptr
vb = std::make_unique
(); // Derived::~Derived() will not be called } ``` --- - したがって、STLコンテナなどの仮想デストラクタをもたないクラスから、動的ポリモーフィズムを目的とする`public`継承をしてはいけない。 - 動的ポリモーフィズムを目的とする`public`継承でなければ、仮想デストラクタをもたないクラスの継承が有効な場面はあります。 ```C++ class YourQueue : protected std::queue
{ // Your implementation ... }; ``` --- - 仮想関数(仮想デストラクタを含む)をもつクラスは、よくある実装では仮想関数テーブルを指す暗黙のポインタ領域をもつので、クラスのサイズは見かけよりもポインタ1つ分多くなります。 - 仮想関数の導入は動的ポリモーフィズムが必要な場面に限るようにしましょう。 ```C++ class Base { public: virtual ~Base() = default; }; static_assert(sizeof(Base) == sizeof(void*)); ``` --- - ちなみに、デストラクタは純粋仮想にすることができます。 - 他に純粋仮想にする関数がないときは、デストラクタを利用するのもよいです。 ```C++ class ABC { virtual ~ABC() = 0; }; ABC::~ABC() = default; ``` --- layout: true ## 8項 デストラクタから例外を投げないようにしよう (p.39) --- name: item-8 **デストラクタから例外を送出してはいけません。** - 例外が`throw`されると、`catch`されるまでスタックが巻き戻されます。 - このとき、スタック上にあるオブジェクトは、デストラクタが呼び出されて破棄されます。 - このときに呼び出されたデストラクタから新たな例外が送出されると、C++は`std::terminate`を呼び出します。 - `std::terminate`はデフォルトで`std::abort`を呼び出し、プログラムを異常終了させます。 --- - 一般に、特殊メンバ関数が例外を投げる設計はよくないです。 - 特殊メンバ関数は暗黙に呼び出されるものなので、**知らないうちに例外が投げられる**可能性があり、保守が困難になります。 - 最低限、**ムーブは例外を投げない**ようにしましょう。ムーブが例外を投げうると、非常に大きなパフォーマンス上の制約を受けます。 ```C++ class X { X() = default; X(const X&) {} X(X&&) noexcept(false) {} }; std::vector
v; for (int i = 0; i < 100; ++i) { v.emplace_back(); } ``` ??? `vector`の拡張の際、ムーブではなくコピーが発生する。 --- layout: true ## 9項 コンストラクタやデストラクタ内では決して仮想関数を呼び出さないようにしよう (p.44) --- name: item-9 - コンストラクタ:基底クラス -> 派生クラスの順 - デストラクタ:派生クラス -> 基底クラスの順 - 基底クラスのコンストラクタ・デストラクタの段階では、派生クラスは有効な状態ではないので、**動的な仮想関数呼び出しが行われない**ようになっている。 - 直接的には非仮想関数呼び出しでも、その中に仮想関数の呼び出しを含んでいれば、同様の問題がある。 - 紛らわしいので、コンストラクタ・デストラクタにおける仮想関数呼び出しはやめましょう。 --- layout: true ## 10項 代入演算子は`*this`への参照を戻すようにしよう (p.48) --- name: item-10 - 組み込み型の挙動に合わせるために、代入演算子のオーバーロードは`*this`への参照を戻すようにしましょう。 - 実際に、標準ライブラリのすべてのクラスがこの規約に従っている。 ```C++ X& X::operator=(const X&) { // Copy members return *this; } int a, b, c = 0; X x, y, z; a = b = c; x = y = z; ``` --- layout: true ## 11項 `operator=`の実装では、自己代入に備えよう (p.50) --- name: item-11 - コピーを自分で定義するときは、自己代入と例外安全が適切に考慮された*copy-and-swap*イディオムに従うべきです。 ```C++ X& X::operator=(const X& that) { X tmp(that); // コピーコンストラクタ tmp.swap(*this); // swapメンバ関数 return *this; // 10項参照 } ``` - コピーコンストラクタ:メンバの複製 - コードの重複も抑えられる - `swap`メンバ関数:例外安全にメンバを入れ替える - 実装について、[25項 例外を投げない`swap`を考えよう](#item-25)も参照 --- - 場合によっては、次のように実装すると便利なこともある。 - コピー代入とムーブ代入を一度にカバーできる。 ```C++ X& X::operator=(X that) { that.swap(*this); return *this; } ``` --- layout: true ## 12項 コピーするときは、オブジェクトの全体をコピーしよう (p.54) --- name: item-12 - 派生クラスのコンストラクタにおいて、基底クラスやデータメンバのコンストラクタを省略すると、暗黙にデフォルトコンストラクタを呼び出す。 - 書き忘れないように。 ```C++ class Y : public X { public: Y(const Y& that) : X(y) {} // ... }; ``` --- - 代入などの場合でも、基底クラスの代入は明示的に行う必要がある。 - ただし、派生クラスの代入は`default`定義で作られるようにすべき。 ```C++ class Y : public X { public: Y& operator=(const Y&) = default; Y& operator=(Y&& that) { X::operator=(std::move(that)); // ... return *this; } // ... }; ``` --- - ごちゃごちゃ言ってますが、[5項](#item-5)でも言ったように、通常こういった処理をする必要はないです(*Rule of zero*)。 - 特に例外を投げないようにすれば、正しいコードを書くのが簡単になります。 - 逆に、例外を投げうるコードを正しく書くのは非常に難しいです。 --- template: toc-header name: chapter3 - 第1章 C++に慣れよう - 第2章 コンストラクタ、デストラクタ、コピー代入演算子 - **第3章 リソース管理** - 第4章 デザインと宣言 - 第5章 実装 - 第6章 継承とオブジェクト指向設計 - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- layout: true ## 13項 リソース管理にオブジェクトを使おう (p.60) --- name: item-13 - リソース管理オブジェクトとは、コンストラクタ引数にリソースを取り、デストラクタでそのリソースを解放するオブジェクトのこと。 - この技法のことを*RAII (Resource Acquisition Is Initialization)*と呼ぶ。 - この技法の本質は、例外送出を含めたいかなる状況においても、**デストラクタがコンパイラによって適切に呼び出される**ことです。 --- - ヒープ領域などのリソースはリソース管理オブジェクトを用いて管理しましょう。 - 標準ライブラリには`std::unique_ptr`や`std::shared_ptr`などのリソース管理クラスがあるので、これらを用いるとよいです。 - cf. *Effective Modern C++ 4章 スマートポインタ* ```C++ auto p = std::make_unique
(); ``` --- layout: true ## 14項 リソース管理クラスのコピーの振る舞いはよく考えて決めよう (p.65) --- name: item-14 - 万が一、自前でリソース管理クラスを作るときには、設計をよく考えましょう。 - リソース管理クラスはコピーが適切でない場合が多い。 - ムーブさえできれば、コピーがなくても便利に扱うことができる。 - C++11以前だったりして、ムーブが使えない場合は、デフォルトコンストラクタと`swap`を提供しておくと、ムーブを模倣した使い方ができる。 ```C++ int main() { using std::swap; Bitmap src("img.bmp"); Bitmap dst; swap(src, dst); } ``` --- - ただし、ほとんどの場合はリソース管理クラスは**自分で作る必要はありません**。 - 例外安全やスレッドセーフなどを考え始めると、素人には非常に困難です。 - 既存のリソース管理クラスを組み合わせて作れるのなら、コピーやムーブはデフォルトに任せても問題ないでしょう。 --- - カスタムデリータを利用すれば、スマートポインタだけで幅広い種類のリソースに対応できます。 ```C++ #include
#include
constexpr auto file_close = [](FILE* fp) { if (fp) { std::fclose(fp); } }; auto make_file(const std::string& name, const char* mode) { return std::unique_ptr
( std::fopen(name.c_str(), mode), file_close); } ``` --- layout: true ## 15項 リソース管理クラスには、リソースそのものへのアクセス方法を付けよう (p.69) --- name: item-15 - もし、自分でリソース管理クラスを作ったなら、生のリソースへアクセスする方法も提供するとよいです。 - 世の中には生のリソースを要求するAPIがあります。 - 生のリソースへのアクセス方法として、**型変換による方法は使わない**ようにしましょう。 - `get`などのメンバ関数として実装するのが自然です。 - 一般に、安全でない処理を暗黙に行うのはよくないです。 --- layout: true ## 16項 対応する`new`と`delete`は同じ形のものを使おう (p.73) --- name: item-16 - `new`と`delete`、`new[]`と`delele[]`の対応を誤ると、**未定義動作を引き起こします**。 - 配列版の`new[]`と`delete[]`は配列の長さの情報も管理しますが、`new`と`delete`はそれを行いません。 --- - そもそも、みなさんは**`delete`および`delete[]`を書かないでください**。 - これらはスマートポインタの中で自動的に呼び出されるようにすべきものです。 - ファクトリ関数(`std::make_unique`など)の外では、**`new`や`new[]`も書かないようにしましょう**。 - そうすれば、`new`と`delete`の対応を間違えることもありません。 - [17項](#item-17)で説明するようなリソースリークの心配もありません。 --- layout: true ## 17項 `new`で生成したオブジェクトをスマートポインタに渡すのは、独立したステートメントで行うようにしよう (p.75) --- name: item-17 - C++は**関数の引数の評価順序を定めていません**。 - したがって、`new`などのリソースの生成と、例外を投げうる関数の呼び出しとを、同じ関数呼び出しの引数でまとめて行うと、リソースリークが起こることがあります。 ```C++ f(std::unique_ptr
(new T), g()); ``` - `g()`が例外を投げる関数のとき、コンパイラがこの式を`new T`、 `g()`、 `std::unique_ptr
()`の順に評価すると、**`new T`がリークします**。 --- - この例は単なる関数呼び出しでしたが、演算子オーバーロードされた2項演算子や、メソッドチェインなどでも同様の問題がある。 ```C++ std::cout << f(new T) << g(); // operator<<(operator<<(std::cout, f(new T)), g()) x.f(new T).g(h()); // g(f(x, new T), h()) ``` --- - 対策は簡単で、リソース確保を行う式は、それのみを行うように書き換えればよい。 ```C++ std::unique_ptr
p(new T); f(std::move(p), g()); ``` - そもそもスマートポインタを返す関数を使っていれば、ここでのような問題は発生しない。 ```C++ f(std::make_unique
(), g()); ``` --- template: toc-header name: chapter4 - 第1章 C++に慣れよう - 第2章 コンストラクタ、デストラクタ、コピー代入演算子 - 第3章 リソース管理 - **第4章 デザインと宣言** - 第5章 実装 - 第6章 継承とオブジェクト指向設計 - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- layout: true ## 18項 インタフェースは、正しく使うときには使いやすく、間違った使い方では使いにくいものにしよう (p.78) --- name: item-18 非常に重要なデザインの指針ですが、言うは易し行うは難しです。日頃から心がけましょう。 --- - 例 ```C++ Date::Date(int month, int day, int year); Date d(30, 3, 1995); ``` - `int`という汎用的な型でなんでも表すのではなく、**型を細かく分ける**ことでケアレスミスを防ぐことができます。 ```C++ Date d(Day(30), Month(3), Year(1995)); // compile error ``` --- - `Month`を列挙型にしたり、名前をつけた定数インスタンスを作るともっとよいかもしれません。 - [4項](#item-4)に従うなら、`static const`メンバ変数よりも`static`メンバ関数のほうがよいかもしれません。 ```C++ class Month { public: static Month March() { return Month(3); } // ... private: explicit Month(int m); // ... }; Date d(Day(30), Month::March(), Year(1995)); ``` --- - STLなどでは、同じ機能のメンバ関数は同じ名前、同じシグネチャをもつように設計されています。 - 同じ名前にしておくと、テンプレートを用いて静的ダックタイピングを行うこともできます。 - このように**一貫性をもった設計**は、使いやすく、保守性の高いシステムをもたらします。 ```C++ template
auto begin(C& c) -> decltype(auto) { return c.begin(); } template
T* begin(T(&a)[N]) { return a; } ``` --- - クラスの機能はそれ**単体で完結**しているべきです。 - ユーザが何かを覚えておかなければならなかったりするような設計は誤用につながります。 - リソースの管理などを人に任せるのではなく、クラスが管理するスマートポインタはその最たる例です。 --- layout: true ## 19項 クラスのデザインを型のデザインとして考えよう (p.85) --- name: item-19 - C++でプログラミングをするときは、クラスつまり型の定義に多くの時間を費やしているはずです。 - **よいクラスのデザイン**は、直感的にわかりやすく効率的な、**よいプログラム**につながります。 - 簡単ではありませんが、*Effective C++*に載っているような問(pp.85-86)について考えることで、見通しがよくなるかもしれません。 --- layout: true ## 20項 値渡しより`const`参照渡しを使おう (p.87) --- name: item-20 - 基本的に**関数の引数は`const`参照**をとるようにしましょう。 - 無駄なコピーや一時オブジェクトの生成を減らせます。 - スライス問題(派生クラスのオブジェクトを基底クラスで受け取るとスライスされる)を避けることができます。 - 組み込み型、イテレータ、関数オブジェクトは基本的に値渡しします。 --- layout: true ## 21項 オブジェクトを戻すべき時に参照を戻そうとしないこと (p.91) --- name: item-21 - **ローカル変数への参照を返してはいけません。** - ラムダキャプチャを使うと気づかないうちにやってしまいがちなので、気をつけるようにしてください。 ```C++ auto f() -> decltype(auto) { auto g = [](){ return 0; }; return [&g](){ return g(); }; } ``` --- - 多くの場合、*RVO (Return Value Optimization)*が効くので、戻り値を値渡しで返しても、一時オブジェクトが発生しないことが多いです。 ```C++ std::string input() { std::string buf; std::cin >> buf; return buf; } ``` --- layout: true ## 22項 データメンバは`private`宣言しよう (p.96) --- name: item-22 - `public`なデータメンバはクラスのインターフェースとして固定化されてしまいます。 - メンバ関数を通じてアクセスするようにしておけば、実装の詳細が変更されてもユーザコードに影響を与えずにすみます。 - `protected`にしても問題は解決しません。**`private`にしましょう**。 ```C++ template
T& get0(std::pair
& p) { return p.first; } ``` --- layout: true ## 23項 メンバ関数より、メンバでも`friend`でもない関数を使おう (p.100) --- name: item-23 - 多くの作業を一度に行うメンバ関数は**設計がよくない**です。 - 作業を細かく分けて、それぞれをメンバ関数として実装するべきです。 - それらのメンバ関数を呼び出す非メンバ関数を提供しておけば、利便性を損ないません。 --- layout: true ## 24項 すべての引数に型変換が必要なら、メンバでない関数を宣言しよう (p.104) --- name: item-24 - 2項演算子のオーバーロードをするときは、非メンバ関数として提供すると、コードの重複を減らせることがある。 - その2項演算子が`private`メンバにアクセスせずに実装できるなら、`friend`にする必要はない。 - `friend`関数は結合を密にするので、**できるだけ避けるべき**です。 --- layout: true ## 25項 例外を投げない`swap`を考えよう (p.108) --- name: item-25 - 非テンプレートクラスのより効率のよい`swap`を自分で定義したいときは、メンバ関数の`swap`を`std::swap`のテンプレート特殊化で呼び出すようにするとよい。 ```C++ class X { X* x; void swap(X& that) { using std::swap; swap(this->x, that.x); } }; namespace std { template <> void swap
(X& a, X& b) { a.swap(b); } } ``` --- - テンプレートクラスの`swap`を自分で定義したいときは、そのクラスが属する名前空間内に非メンバ関数の`swap`を実装するとよい。 - **`std`名前空間のテンプレートの部分特殊化は認められていない**。 - *ADL (Argument Dependent Lookup)*により、コンパイラがこの非メンバ関数`swap`を見つけてくれる。 ```C++ namespace my { template
class X { T* x; void swap(X& that) { using std::swap; swap(this->x, that.x); } }; template
void swap(X
& a, X
& b) { a.swap(b); } } ``` --- - `swap`を呼び出すときはいつも、そのローカルスコープ内で`using std::swap`し、`swap`をスコープ解決しないで呼び出しましょう。 - スコープ解決しないことで、適切な`swap`をコンパイラに探索(ADL)させることができます。 - `using std::swap`することで、`swap`の探索範囲に`std::swap`を追加することができます。これにより、他の名前空間に`swap`が見つからない時に`std::swap`が呼ばれるようになります。 ```C++ template
void f(T& a, T& b) { // ... using std::swap; swap(a, b); // ... } ``` --- `swap`は例外安全の要です。`swap`は**必ず例外を投げない**ように実装しましょう。 --- template: toc-header name: chapter5 - 第1章 C++に慣れよう - 第2章 コンストラクタ、デストラクタ、コピー代入演算子 - 第3章 リソース管理 - 第4章 デザインと宣言 - **第5章 実装** - 第6章 継承とオブジェクト指向設計 - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- layout: true ## 26項 変数の定義は可能な限り先延ばししよう (p.117) --- name: item-26 - 変数はその初期値が生成される時点で定義するようにしましょう。 - プログラムが読みやすくなり、無駄なコンストラクタ・デストラクタ呼び出しをなくせる可能性があります。 - cf. [4項 オブジェクトは、使う前に初期化しよう](#item-4) --- - コンストラクタ・デストラクタの処理が重いオブジェクトをループ内で作る場合は、ループの外で初期化するほうが効率がよいこともあります。 ```C++ std::string cat; for (int i = 0; i < n; ++i) { const auto token = [](){ std::string buf; std::cin >> buf; return buf; }(); cat += token; } std::string buf; for (int i = 0; i < n; ++i) { std::cin >> buf; cat += buf; } ``` --- layout: true ## 27項 キャストは最小限にしよう (p.120) --- name: item-27 - キャストは型システムをだめにします。**極力キャストは使わない**ようにしましょう。 - **Cスタイルのキャストは使わない**ようにしましょう。通常は `static_cast` を使いましょう。 - 型変換は実行コストをもつことがあるので、より明示的な方法で行うべきです。 ```C++ double x = 3.14; int y = static_cast
(d); ``` --- - 派生クラスと基底クラスの間のポインタ変換においては、そのアドレス値が変化することがあります。 - 特に多重継承をしたときに、ポインタ値を調整する必要があることがあります。 - 一般に`dynamic_cast`を多用するコードは設計が悪いです。 - 仮想関数呼び出しに置き換えるなど、設計を考えなおしてください。 ```C++ struct B1 { virtual ~B1() = default; }; struct B2 { virtual ~B2() = default; }; struct D : public B1, public B2 {}; D *d = new D; B1 *b1 = d; B2 *b2 = d; ``` --- layout: true ## 28項 オブジェクト内部のデータへの「ハンドル」を戻さないようにしよう (p.128) --- name: item-28 - オブジェクト内部を指す参照、ポインタ、イテレータなどのハンドルを返す関数を書くときには、バグを生まないように注意しましょう。 - 戻り値を変更できるべきでない場合は、`const`なハンドルを返しましょう。 - cf. [3項 可能ならいつでもconstを使おう](#item-3) - 内部へのハンドルはカプセル化を弱めるので避けましょう。 - ダングリング(ぶらさがり)ハンドルになる可能性を考慮しましょう。 ```C++ const int& f() { std::vector
v = {0, 1, 2}; return v.front(); } ``` --- layout: true ## 29項 コードを例外安全なものにしよう (p.132) --- name: item-29 - 例外安全なコードは、例外が投げられても次の性質を保ちます。 - **リソースリークをおこさない** - データ構造が**無効な状態にならない** - リソースリークについては、RAIIを用いるなどして対処しましょう。 --- 例外安全な関数は次のいずれか1つを保証しなければなりません。 - *基本保証 (basic guarantee)* - *強い保証 (strong guarantee)* - *不送出保証 (nothrow guarantee)* --- *基本保証* - 例外が投げられても、プログラムのすべてのオブジェクトの**データ構造が有効に保たれる**こと。 - プログラムの状態は有効であれば変化してもよい。 *強い保証* - 例外が投げられると、プログラムの状態が例外を投げた関数の呼び出し前に戻ること。 - 呼び出しが**成功した状態**か、呼び出す**前の状態**かの**いずれかにしかならない**。 *不送出保証* - **例外を投げない**こと。 --- - 当然ながら、例外を使わずにすむならそれに越したことはありません。 - 例外を使わざるを得ないときも、出来る限り強い保証をするように心がけるべきです。 - 最低限、基本保証は心がけよう。 --- - 関数に`noexcept`指定することは不送出保証をすることではありません。 - `noexcept`指定された関数が例外を投げると、`std::terminate`が呼び出されます。 - `std::terminate`はデフォルトで`std::abort`を呼び出し、プログラムを異常終了させます。 - *copy-and-swap*イディオムを用いると、強い保証をするのが容易になります。 - copyにより呼び出し前の状態を複製し、不送出保証を満たすswapを用いる。 copyにはしばしばRAIIを用いる。 ```C++ void f() noexcept { throw; // std::terminate -> std::abort } ``` --- layout: true ## 30項 インラインをよく理解しよう (p.140) --- name: item-30 - cf. [2項 `#define`より、`const`、`enum`、`inline`を使おう](#item-2) - インライン化すると、関数呼び出しがその関数の定義で置き換えられます。 - 関数の定義が短ければ、インライン化によって関数呼び出し命令やスタック管理の分だけオブジェクトサイズが小さくなります。 - オブジェクトサイズが小さいと、キャッシュヒット率が上がります。 --- - コンパイル時インライン化を有効にするには、コードの定義をヘッダに書く必要があります。 - そのため、テンプレートはインライン化しやすいです。 - 一般に、関数ポインタを介した関数呼び出しはインライン化されません。 --- layout: true ## 31項 ファイル間のコンパイル依存性をなるべく小さくしよう (p.146) --- name: item-31 - クラスを定義するには、データメンバや基底クラスの完全な定義が必要なことがある。 - スタック上でのそのクラスのサイズがコンパイラにわかる必要がある。 - 定義の書かれたヘッダをインクルードすることで、ヘッダの依存関係が生まれる。 - ポインタや参照ならば、宣言で十分である。 - ポインタや参照のサイズはコンパイラが知っている。 ```C++ #include
class Y; class X { std::vector
x; Y *y; }; ``` --- - 関数宣言のときには、引数や戻り値のクラスの前方宣言で十分です。 - その関数が呼び出されるときには、そのクラスの定義が提供されているはずです。 - 標準ライブラリのクラスを利用するときは、前方宣言ではなく単にインクルードするとよいです。 --- - 宣言と定義は別のファイルに書きましょう。 - テンプレートは定義を含めてヘッダに書く必要があります。 - `export`のことは忘れましょう。 - pimplイディオムを用いると、コンパイル依存性を最小限にできます。 - 代償として一定の実行時コストがかかります。 ```C++ class Ximpl { // ... }; ``` ```C++ class Ximpl; class X { // ... std::unique_ptr
pimpl; }; ``` --- template: toc-header name: chapter6 - 第1章 C++に慣れよう - 第2章 コンストラクタ、デストラクタ、コピー代入演算子 - 第3章 リソース管理 - 第4章 デザインと宣言 - 第5章 実装 - **第6章 継承とオブジェクト指向設計** - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- layout: true ## 32項 `public`継承はis-a関係を表すようにしよう (p.158) --- name: item-32 - C++の継承の仕組みは構文的な側面が強く、オブジェクト指向を直接的にモデル化しているわけではありません。 - そのため、さまざまなオブジェクト指向上の概念を表現するために、少ない言語仕様を組み合わせたイディオムを用いる傾向があります。 --- - is-a関係は一般に`public`継承を行うときの指針として知られていますが、is-a関係が成り立つクラスが常に継承できるというわけではありません。 - 派生クラスが基底クラスから**正しく**`public`継承できるのは、基底クラスのすべての仮想関数に対して、対応するオーバーライド関数が次の条件を満たすときである。 - 基底クラスの仮想関数の引数に渡すことのできるオブジェクトを、派生クラスのオーバーライド関数の引数にも常に渡すことができる。 - 派生クラスのオーバーライド関数の戻り値が満たす性質を、基底クラスの仮想関数の戻り値が常に満たしている。 - 要するに、基底クラスで成り立つ性質は、すべて派生クラスでも成り立つ必要があり、**そのときに限り**`public`継承を行える。 --- layout: true ## 33項 継承した名前を隠蔽しないようにしよう (p.164) --- name: item-33 - 継承関係にあるクラスは、それぞれスコープをもちます。 - 派生クラスのメンバ関数のブロック中では、基底クラスの名前は派生クラスの名前で隠蔽されてしまうので気をつけましょう。 - 特にメンバ関数において、シグネチャの異なる関数であっても隠蔽は発生します。 ```C++ class Base { public: virtual void f() {} void f(int) {} }; class Derived : public Base { public: void f() override { f(0); // compile error } }; ``` --- - `using`宣言を用いると、基底クラスのメンバ関数を取り込んで、隠蔽しないようにすることもできます。 - 一部のメンバ関数のみを取り込みたいときは、派生クラスにおいて転送関数を定義すればよい。 ```C++ class Base { public: virtual void f() {} void f(int) {} }; class Derived : public Base { public: using Base::f; void f() override { f(0); } }; ``` --- layout: true ## 34項 インタフェースの継承と実装の継承の区別をしよう (p.170) --- name: item-34 - 純粋仮想関数はインタフェースの継承を表現する。 - 仮想関数はインタフェースとデフォルト実装の継承を表現する。 - 非仮想関数は継承において変わらない実装を表現する。 --- - 純粋仮想関数は実装をもつことができる。 - 呼び出すときは明示的にスコープ解決する必要がある。 ```C++ class X { public: virtual ~X() = default; virtual void f() = 0; }; void X::f() { // default implementation } class Y : public X { public: void f() override { X::f(); } }; ``` --- - 継承はインタフェースを派生クラスに強制させるために用いるべきです。 - 実装の継承を行うために用いるのはよい設計ではないと思う。 - 実装の重複を減らすことは、継承以外の方法で行うべきです。 --- layout: true ## 35項 仮想関数の代わりになるものを考えよう (p.179) --- name: item-35 - `public`な非仮想関数から`private`な仮想関数を呼び出す手法のことを*NVI (Non-Virtual Interface)イディオム*と言う。 - 仮想関数の呼び出しをNVIイディオムにしたがって呼び出すようにすると、疎結合になって保守しやすくなります。 ```C++ class X { public: virtual ~X() = default; void f() { f_impl(); } private: virtual void f_impl() = 0; }; class Y : public X { void f_impl() override {} }; ``` --- - 関数オブジェクトの利用や、ストラテジパターンは仮想関数の代替になり得ます。 ```C++ class X { public: X(std::function
f) : func_(f) {} void f() { func_(); } private: std::function
func_; }; ``` --- layout: true ## 36項 非仮想関数を派生クラスで再定義しないこと (p.188) --- name: item-36 - 非仮想関数を派生クラスで再定義すると、基底クラスのインタフェースをこわすことになるので、絶対にやめましょう。 - それは継承でもなんでもありません。 ```C++ class X { public: int f() { return 0; } }; class Y : public X { public: int f() { return 1; } }; ``` --- layout: true ## 37項 継承された関数のデフォルト引数値を再定義しない (p.190) --- name: item-37 - 仮想関数は動的結合だが、デフォルト引数は静的結合なので、**仮想関数にデフォルト引数を指定しない**ようにしましょう。 - NVIイディオムを用いていれば、問題を解消できます。 ```C++ class X { public: virtual ~X() = default; int f(int x = 0) { return f_impl(x); } private: virtual int f_impl(int x) = 0; }; class Y : public X { int f_impl(int x) override { return x; } }; ``` --- layout: true ## 38項 コンポジションを使ってhas-a関係、is-implemented-in-terms-of関係を作ろう (p.194) --- name: item-38 - 継承は基底クラスと派生クラスの間に**密結合**をもたらします。 - コンポジションで実装するほうが疎結合なので、コンポジションを優先したほうがよいです。 --- layout: true ## 39項 `private`継承は賢く使おう (p.198) --- name: item-39 - 多くの場合、`private`継承でできることはコンポジションでも実現できます。 - 疎結合になるコンポジションを優先しましょう。 - `private`継承が意味をもつのは*空の基底クラスの最適化*を使うときです。 - 空のクラスをコンポジションでもつと、そのクラスのサイズは1バイトになります。 - 継承でもつと、派生クラスのサイズが増えません。 --- layout: true ## 40項 多重継承は賢く使おう (p.203) --- name: item-40 - 多重継承は使いこなすのが難しい機能です。よく考えて設計しましょう。 - 空のクラスや抽象基底クラスの多重継承は、問題ないことが多いと思います。 - 菱型継承はさらに難しい問題をはらんでいます。 - 可能なら、菱型継承を避けるべきだと思います。 ```C++ class B { public: virtual ~B() = 0; }; class D1 : public B {}; class D2 : public B {}; class D : public D1, public D2 {}; ``` --- - どうしても菱型継承を行いたいなら、そのクラスの意味をよく考えて、適宜仮想継承の機能を用いましょう。 - 仮想継承は、菱型継承の仮想基底クラスが継承のパスの数の分だけ複製されてしまう問題を防ぎますが、そのための実行時コストがかかります。 - 派生クラスは常に仮想基底クラスの初期化の責任をもつ必要があります。 - メンバをもたないクラスを菱型継承の仮想基底クラスにしておけば、管理コストを増やさずにすむでしょう。 ```C++ class D : virtual public D1, virtual public D2 {}; ``` --- template: toc-header - 第1章 C++に慣れよう - 第2章 コンストラクタ、デストラクタ、コピー代入演算子 - 第3章 リソース管理 - 第4章 デザインと宣言 - 第5章 実装 - 第6章 継承とオブジェクト指向設計 - 第7章 テンプレートとジェネリックプログラミング - 第8章 `new`と`delete`のカスタマイズ - 第9章 いろいろな事 --- name: conclusion layout: false ## Conclusion この講座で言いたかったこと - .huge[*Effective C++*は古いが役に立つ]