このドキュメントはサイボウズ社内のトレーニング用に作成したものです。 |
モダンとはテンプレートメタプログラミング(TMP)を駆使することです。嘘です。
宗教論争に意味はないので、ここでは
「最近の C++ の仕様・機能を理解し、C より実装効率が良く不具合の少ない」
プログラミング技法を「モダン C++ プログラミング」と定義します。
つまり、不具合が少なく、かつ C にはもう戻れなくなるような効率の良さを達成するものです。
学習効率(ROI)が極めて良くないような技法(例えば TMP)は、この定義では除外されます。
お勧めの順序は以下。決して全部を読もうとしないこと。
その他リファレンス
これで十分、いいコードが書けるようになります。
絶対にやったほうがいいことだけまとめておきます。
const
はきちんとつけるC++の価値の 20% くらいは const
にあります。
const
は安全なプログラムを作る上で不可欠な機能ですが、使い方を知らないとコンパイラに怒られまくって嫌になります。
正しい使い方を学びましょう。
#define BLOCK_SIZE 4096 void print(std::string& s); class C { public: C(int bufsiz) { buffer_size = bufsiz; } int size() { return buffer_size; } // const メソッドにするべき private: int buffer_size; // const にすべき }; |
const int BLOCK_SIZE = 4096; void print(const std::string& s); class C { public: C(int bufsiz): buffer_size(bufsiz) {} // const メンバーは初期化子で初期化 int size() const { return buffer_size; } // const の位置に注意 private: const int buffer_size; }; |
const
なポインタ例えばバッファの先頭位置を必ず差すポインタはどう書くか?
void f( const std::string& s ) { const char* p = s.c_str(); // const? printf("hoge %s", p); p = "fuga"; // 代入できるので const じゃない。 } |
正しくはこう。
void f( const std::string& s ) { const char* const p = s.c_str(); // p は代入不可 ... } |
const
な STL コンテナconst
な STL コンテナ(vector 等)をイテレートするには通常の iterator
じゃなく、const_iterator
を使います。
iterator には逆順(reverse_iterator
)もありますが、そちらも const
の場合は const_reverse_iterator
を使います。
void f( const std::vector<std::string>& v ) { for( std::vector<std::string>::const_iterator it = v.begin(); it != v.end(); ++it ) { ... } // for( const std::vector<std::string>::iterator it = v.begin(); // とかするのは間違い。iterator 自体は const じゃないので } |
explicit
をつける一引数をとるコンストラクタは、暗黙的な型変換に使われて思いもよらないことが起こることがあります。
暗黙の型変換を抑止するために、一引数のみのコンストラクタには explicit
を指定してください。
C++ の設計ミス(デフォルトを explicit にするべきだった)の一つでしょう。
class C { public: C( int i ) { ... } // int が暗黙で C に変換される可能性がある ! explicit C( int i ) { ... } // explicit をつければ、暗黙の型変換は防止できる C() { ... } // デフォルトコンストラクタにはつけなくていい C( int i, long l ) { ... } // 二引数以上でもつけなくていい private: C( char c ) { ... } // もちろん private なら explicit しなくていい }; |
コンストラクタには、お作法があります。以下は鉄則として考えておきましょう。
primitive は明示的に初期化する
スタックオブジェクトでは(2013-12-18 21:32 訂正、newでも同様と指摘を頂きました)
int 等の primitive 型のメンバーは初期化されません。
明示的に初期化をしないと、思わぬ事故につながるので、必ず初期化しましょう。
struct C { C() {} // i は 0 初期化されない! int i; }; int f() { C c; return c.i; // 不定 } |
初期化子で必ず全メンバーを初期化する
初期化はコンストラクタのボディでいいと思っていませんか?
初期化子をかかなくても、じつはデフォルトコンストラクタでメンバーは初期化されています。
また、const や参照メンバーは初期化子でなければ初期化できません。必ず初期化子で全メンバーを初期化しましょう。
class C { public: C(const std::string& s) { m_name = s; // 初期化でなく代入。もし m_name が const ならコンパイルできない。 m_i = 0; // コンパイルエラー } C(const std::string& s): m_name(s), m_i(0) {} // good private: std::string m_name; const int m_i; }; |
初期化子の記述順序は、メンバーの定義順にする
実は初期化子の記述順序は、初期化の順序と無関係です。メンバーは、メンバーが定義された順に初期化されます。
なので、メンバーの定義順と異なる初期化子の記述順序はバグのもとになります。
class C { public: C( std::size_t size): m_buf(size), m_top(&(m_buf[0])), m_bottom(m_top + size) {} // m_bottom の定義が先なので、m_top 初期化前にこちらが実行される private: std::vector<char> m_buf; char* const m_bottom; char* const m_top; }; |
継承されるクラスのデストラクタには virtual
つけないといけません。
継承の可能性があるクラス(シングルトンなどは継承されないでしょう)では、virtual
つけるのを原則としましょう。
つけないと、親クラスにアップキャストされたオブジェクトのデストラクタが正常に走りません。
親クラスのデストラクタは自動的に呼び出されるので、サブクラスのデストラクタ内で明示的に呼び出す必要はありません。
class A { public: A() {} virtual ~A() {} // 処理が空でも virtual つける }; class B { public: B() {} ~B() { // no need to call ~A(). } }; |
C++ で Java のような言語にあるインタフェースが欲しい場合、通常は純粋仮想クラスとして実装します。
ところが純粋仮想クラスはあくまでクラスであってインタフェースではないため、このようなときも仮想デストラクタを定義するべきです。
|
覚えることは四つ。
using
禁止cpp ファイルでは無名名前空間でローカルシンボルをくくる
C では static
でしたが、C++ では無名名前空間を使うのがスマートです。
const int CONSTANT = 3; // const はデフォルトでファイルローカルスコープ namespace { // 無名の名前空間 void do_something() { // 無名名前空間の名前は他のコンパイル単位からは参照不可能 } } int main(int argc, char** argv) { // main は見えるようにする do_something(); return 0; } |
C ライブラリも std
名前空間へのラッパーを極力使う
c.f. http://www.cplusplus.com/reference/clibrary/
#include <cstddef> // std::size_t など #include <cstdlib> // std::malloc, std::free など |
※2013-12-27 std::errno
は存在しないので修正しました
C++ 以外にも言えることですが、コンパイラはの警告は 0 にしておきましょう。放置すると、深刻なものも見逃します。
g++
なら -Wall -Wnon-virtual-dtor -Woverloaded-virtual
をつけるのを推奨します。
-Wall
-Wnon-virtual-dtor
-Woverloaded-virtual
const
の有無の違い等で override に失敗して overload になっているメソッドがあれば警告。Java などのオブジェクトと異なる C++ 一番の注意点(利点でもある)は、ヒープオブジェクトとスタックオブジェクトの二種類が存在することです。ヒープオブジェクトは new
で確保したオブジェクトのことで、これに関して注意することは後で解説します。ここではスタックオブジェクトに関して注意しておきます。
スタックオブジェクトとは、new
で確保しないオブジェクトで、存在するスコープをすぎるとすぐにデストラクタが呼ばれて使えなくなります(これを利用するテクニックが後述する RAII です)。スタックオブジェクトの生存期間を超えたら参照は無効化されるので、そのような場合はコピーする必要があります。
class A { public: A(const std::string& s): m_s(s) {} void print() const { std::cout << m_s << std::endl; } private: const std::string& m_s; // 参照を保持 }; void foo() { A a("aaa"); // "aaa" が暗黙的に std::string になるが、 a.print(); // 一時オブジェクトなのでここでは既に無効 } |
これをコピーするようにするのは簡単で、メンバーの m_s
を参照でなくせば良いです。
class A { public: A(const std::string& s): m_s(s) {} void print() const { std::cout << m_s << std::endl; } private: const std::string m_s; // コピーする }; void foo() { A a("aaa"); a.print(); // OK } |
そこでコピーだから安全と参照をやめていくと、コピー過多で遅いコードになります。
先の例でコピーをせずに動かすには、一時オブジェクトの生存スコープを少し延ばします。
class A { public: A(const std::string& s): m_s(s) {} void print() const { std::cout << m_s << std::endl; } private: const std::string& m_s; // 参照を保持 }; void foo() { std::string t = "aaa"; A a(t); a.print(); // OK } |
スコープが明確にできない場合は後述する参照カウント式のスマートポインタ( |
上記で「一時オブジェクトはスコープをすぎるとすぐにデストラクタが呼ばれて使えなくなる」と言及しましたが、C++ ではそのスコープは一時オブジェクトが含まれる式全体(full-expression)と定義されています。なので、以下のコードは大丈夫です。
std::string foo() { std::string s = "/etc/fstab"; return s; } void foo() { int fd = open( foo().c_str(), O_RDONLY ); ... } |
foo()
が返す一時オブジェクトである string から c_str()
で const char*
なポインタを open(2)
に渡しているわけですが、ポインタを作ったあとも一時オブジェクトの string は open の呼び出しが完了するまで生き続けます。
c.f.
std::string
のススメモダン C++ では C 文字列の代わりに std::string
が使われます。
大事なことだからもう一度。
モダン C++ では C 文字列の代わりに |
例えば以下のような C のコードは、
void f(const char* s) { ... } void g(const char* s2) { char buf[256]; snprintf(buf, sizeof(buf), "Hello %s", s2); f(buf); } |
std::string
を使う C++ なら以下のように書けます。
void f(const std::string& s) { ... } void g(const std::string& s2) { f("Hello " + s2); } |
文字列比較も ==
などで書けます。strcmp
はさようなら。
bool is_hello(const std::string& s) { return s == "Hello"; } |
C 文字列(const char*
)は const std::string
への暗黙の型変換が用意されているので、const std::string
を渡す場面でも C 文字列を使うことができます。
void f(const std::string& s) { ... } void g() { f("Hello world!"); // 暗黙的に std::string に変換される } |
std::string
が使えない数少ないケースとして fstream ライブラリでは、ファイル名に C 文字列しか受け付けません。これは STL が整備される前の古い時代の名残で、後述する C++11 ではstd::stringでも大丈夫になりました。
void f(const std::string& s) { std::ofstream out(s.c_str()); // C 文字列にしないといけない } |
|
cybozu::String
https://github.com/herumi/cybozulib
@herumi 謹製の、ユニコードを扱える std::string
互換クラスライブラリ。UTF-8 の文字列を便利に使えます。
テストコードをみればだいたいわかるはず。
新規に書くコードにおいては、例外は使うべきものです。C のように関数の返り値をいちいちチェックするコードを省け、後述する RAII テクニックによって一時ファイル等のゴミを後片付けしながらきちんとエラー処理をすることができるからです。
ただ、初期の C++ では例外がなかったため、Google 等一部の例外安全でないコード資産が多くある会社では例外を使えないところもあります。 このドキュメントはモダン C++ を前提にしているので、今日すでに一般的に実装されている例外を活用します。 |
C++ ではコピー可能なものなら何でも throw することができますが、モダン C++ では原則として std::exception
の派生クラスを throw します。標準例外クラスは stdexcept
に用意されているので、適切に利用します。
プログラミングエラー系は std::logic_error
とその標準派生クラス(std::invalid_argument
等)を使えばほとんど十分でしょう。しょせんプログラミングエラーなので、直せば済むことだから。
一方で、正しくプログラミングしても避けられない実行時のエラーは、その原因別に std::runtime_error
の派生クラスを作るべきでしょう。
class protocol_error: public std::runtime_error { public: explicit invalid_packet(const std::string& s): // explicit 付け忘れないように std::runtime_error(s) {} }; void f(const std::string& s) { if( s.empty() ) throw std::invalid_argument("s must not be empty.")' Socket s(...); s.send(s); std::string reply = s.recv(); if( reply != "ok" ) throw protocol_error("not ok."); } |
例外はスタックオブジェクトを throw
し、その const
参照で catch
するのが原則です。
このスタックオブジェクトはランタイムの専用の領域にコピーされるので、コピー可能でなければなりません。
まあ上記のようなクラスはコピー可能なので大丈夫です。
try { ... throw std::runtime_error("hoge!"); // スタックオブジェクト ... } catch( const std::runtime_error& e ) { // const 参照 std::cerr << e.what() << std::endl; } |
catch
することcatch
されなかった例外は最終的に std::terminate
で処理されますが、その場合一切のスタックオブジェクトが破棄されません。結果として、後述する RAII を利用したクリーンアップ処理(ゴミファイルの消去など)が動作しないといった不都合が生じる可能性があります。(c.f. http://ymmt.hatenablog.com/entry/2012/02/01/015040)
典型的な C++ の main 関数は以下のようになるでしょう。
int main(int argc, char** argv) { try { // do something return 0; } catch( const std::exception& e ) { // 標準的な例外オブジェクト // exception handling } catch( ... ) { // その他の例外 ... はすべてをキャッチする構文 // exception handling throw; // catch して rethrow すれば、main 以降のスタックは処理できる // rethrow しないと POSIX スレッドプログラムは abort するので注意。 } return 1; } |
デストラクタから例外を投げること自体は違法ではないものの、デストラクタが例外を投げると例外中に例外が発生する可能性が著しく高まります。この二重例外は違法で、std::terminate()
が呼ばれてプログラムが異常終了してしまうことになります。
というわけで、以下の方針に従ってください。
delete
を呼ぶことはなくなります。エラーは握りつぶすこと
例えば close(2)
が失敗しても、例外を投げたりしてはいけません。
ログに記録することでさえ危険なので、無視していいものは無視しましょう。
class C { public: C(const char* filename): m_fd(open(filename, O_RDONLY)) { if( m_fd == -1 ) throw std::runtime_error("open failed."); } ~C() { if( m_fd != -1 ) close(m_fd); // close が失敗しても気にしない } private: const int m_fd; }; |
後述するルールに従っていれば、コンストラクタでは例外を投げて良いです。 |
コンストラクタで例外を投げたときに起こるのは以下のようなことです。
c.f. |
Java と違い、例外指定がオプションでかつ実行時チェックである C++ では、例外指定は全く無意味であると結論されています。
C++11 では例外指定は deprecate され、例外を絶対に投げないことを保証する noexcept
キーワードが追加されました。コンパイラの最適化用だと考えてください。
Java の例外は checked exceptions と呼ばれ、メソッドシグネチャで発生する可能性がある例外を明示する必要があります。
これはなかなかの難物で、多数の例外の列挙を防ぐために Java では一度 catch して別の例外に wrap して投げなおすというコードが多用されます。
C++ の例外は checked exceptions ではないので、情報を付加する目的がないのであれば、wrap するような面倒なことは避けた方が良いでしょう。
詳しくは以下の資料に目を通してください。
例外安全とは、例外が発生した後もプログラムが安全に走れる保証のことです。例外安全というと、例外を使わなければ関係ないように聞こえますが、実際にはリソースリークなどを確実に防止するので、長時間走るネットワークサーバーやデーモンでも非常に重要な要素となります。
結論からいえば、例外安全なプログラムは以下のルールに従えば作ることができます。
new
しないnew
したポインタはスマートポインタに格納するこのルールを守ればほとんどの場合例外を安全に使うことができるでしょう。
マルチスレッドプログラム等、現在のスタック(スレッド)以外と協調動作するプログラムでは特別な注意が必要になることがあります。 |
RAII (Resource Acquisition Is Initialization) はスタックオブジェクトがそのスコープを離れると自動的にデストラクタが呼ばれ解放されることを利用した、リソース管理のテクニックです。
Java や Python の finally を、わざわざ書かなくても実行してくれるものと考えてください。
class file { public: file(const std::string& filename): m_fd(open(filename.c_str(), O_RDONLY)) { if( m_fd == -1 ) throw std::runtime_error("failed to open" + filename); } virtual ~file() { if( m_fd != -1 ) close(m_fd); } int fileno() const { return m_fd; } int read(char* in, std::size_t len) { std::size_t n_read = 0; while( n_read != len ) { int t = read(m_fd, in, len - n_read); if( t == 0 ) throw std::runtime_error("unexpected end of file"); if( t == -1 ) throw std::runtime_error("failed to read"); n_read += t; } } private: const int m_fd; }; void read(const std::string& filename) { file f(filename); std::vector<char> v(4096); f.read(&(v[0]), 4096); // ファイルが 4096 バイト未満なら例外が飛ぶが、f は close される // 正常なときも f は close される } |
RAII は通常の実行時はもちろん、例外が発生した時にもきちんと実行されるので、例外安全なプログラムを書くためのキーとなるテクニックです。STLコンテナやスマートポインタは RAII を活用するライブラリです。
C++ はオブジェクトをコピーや代入するために、通常は単にメモリの値をコピーするコピーコンストラクタや代入オペレーターを自動的に生成してくれます。
ですが、メモリ以外のリソース、例えばロックやファイルはコピーしにくいものです。
以下の方針に従ってコピー・代入を制御してください。
コピー、代入を禁止するには、コピーコンストラクタと代入オペレーターを private 宣言します。
private にアクセスできるメソッドや friend 関数にも禁止するために、宣言のみで定義はしません。
class NonCopyable { public: NonCopyable() {} virtual ~NonCopyable() {} private: mutable mutex_t m_lock; // mutex は普通コピーできない NonCopyable(const NonCopyable& ); // コピーコンストラクタ NonCopyable& operator=(const NonCopyable& ); // 代入オペレータ }; |
Boost には
|
なお、コピーができないオブジェクトを共有するためには、後述する参照カウント式のスマートポインタを利用してください。
スマートポインタとは、RAII を使って適切に delete
してくれるクラスです。
オペレーターオーバーロードのお陰で、通常のポインタとあまり違いを意識せずに使うことができます。
スマートポインタを使うことで、delete
忘れを防止できます。
特にコンストラクタで二つ以上のオブジェクトを new
するような場合に、スマートポインタに入れることで途中の例外によるメモリリークを防止することができます。
class D {...}; class C { public: C(): m_obj1(new D), m_obj2(new D) {} // m_obj2 のコンストラクタで例外が起きると // m_obj1 がリークしてしまう virtual ~C() { // デストラクタも自分で書かないといけない。 // コンストラクタで例外が発生すると呼ばれないし。 delete m_obj1; delete m_obj2; } private: D* const m_obj1; D* const m_obj2; }; |
このようなときは std::auto_ptr
を利用することでうまく解決ができます。
class C { public: C(): m_obj1(new D), m_obj2(new D) {} // m_obj2 のコンストラクタで例外が起きても // m_obj1 は適切に delete される // デストラクタは不要 private: std::auto_ptr<D> m_obj1; std::auto_ptr<D> m_obj2; }; |
簡単ですね! ただ、std::auto_ptr
には以下のような制限があります。
delete[]
してくれないため)std::unique_ptr
ではできます。C++03 では std::vector
を利用しましょう。C++11 では
|
std::auto_ptr
では制限が厳しいため、boost や POCO や C++11 では参照カウント方式でコピーが可能なスマートポインタが用意されています。
例えば C++11 では std::shared_ptr
として利用することができます。
参照カウント方式なので、循環参照を作るとメモリリークすることには注意してください。
void f(std::vector<Poco::SharedPtr<file> >& v) { Poco::SharedPtr<file> p( new file("hoge.txt") ); v.push_back(p); // コピー可能なので STL コンテナに入れて良い } |
STL は今日の C++ コンパイラでは広く実装・最適化されています。
STLの中でも std::string
や std::vector
といった STL コンテナはメモリ資源を RAII で適切に管理してくれる有用なものです。
STLコンテナを使うことで自分で new
, delete
する場面は著しく減らせるでしょう。
ただし、STLコンテナにはいくつか癖があります。
const
なメンバーを持っているオブジェクトは代入できないため、格納できません。auto_ptr
はコピー可能でないため格納できません。C++11 では |
例えば vector
は連続性が保証されているので、以下のような使い方ができます。
先述したように、string
は C++03 では保証されていません。
int readfile(int fd) { std::vector<char> buf(256); char* const pBuf = &(buf[0]); return read(fd, pBuf, 256); } |
STLコンテナは内容を適切にコピーしてくれるので、例えば std::string
や std::vector
を参照をつけずに渡せます。
ですが、メモリコピーが発生するので、大きなコンテナは参照で渡すようにしましょう。
std::string format(std::vector<std::string> v) { // ここでコピーが発生 std::string t; for( std::vector<std::string>::const_iterator it = v.begin(); it != v.end(); ++it ) { t += *it; t += ", "; } return t; } int main(int argc, char** argv) { std::vector<std::string> v(argc); for( int i = 0; i < argc; i++ ) { v[i] = argv[i]; } std::string s = format(v); // ここでコピーが発生 std::cout << s << std::endl; return 0; } |
void format(const std::vector<std::string>& v, // const 参照 std::string& s) { // 参照 s.clear(); // 空にする for( std::vector<std::string>::const_iterator it = v.begin(); it != v.end(); ++it ) { s += *it; s += ", "; } } int main(int argc, char** argv) { std::vector<std::string> v(argc); for( int i = 0; i < argc; i++ ) { v[i] = argv[i]; } std::string s; format(v, s); std::cout << s << std::endl; return 0; } |
テンプレートは便利な機能ですが、その文法や制限を学ぶのには高いコストがかかります。
このコストはメンテナンスをするチーム全体にかかるものなので、高度なテンプレートライブラリは自作しないようにしましょう。
具体的な指針としては、特殊化をしだすと途端に学習コストが高まります。
特殊化をする前に、オーバーロードで片付かないか検討しましょう。
C++ は仕様が変遷してきたため、ライブラリもその時々のスタイルに合わせて作られています。
ここでは、このドキュメントで定義するモダン C++ に合致するライブラリを紹介します。
具体的には以下の条件を満たすライブラリです。
POCO はネットワークアプリケーションを作成するのに必要な機能を揃えたライブラリです。
SSLを含むネットワーク機能に加え、ファイルシステム・スレッド・時刻・ロギング・設定ファイルなどの機能があります。
テンプレートを多用してはいないため、ソースコードはシンプルで読みやすく、コンパイル速度も速いです。
Boost はテンプレートメタプログラミングを中心とした、非常に高度な C++ の技術を駆使したライブラリです。
C++11 にも多数の仕様が取り込まれる等、広く影響力を持ってはいますが、機能セットとしては POCO と大差がありません。
Boost のプログラムソースは初心者にはとても読めるものではなく、またテンプレートを駆使するためコンパイル速度が非常に遅いです。
特に有用な機能は C++11 に取り込まれていますので、Boost を使うことはお勧めしません。
QT は GUI アプリケーションの作成で広く使われている C++ のフレームワークです。
ただし、古くからあるため例外安全ではありません。
wxWidgets も以前から使われている GUI ライブラリですが、native GUI ではないのでかっこ悪いのが残念。
例外は使わないが、例外安全ではあります。
この辺参照。
C++11 は 2011 年に規格が標準化された新しい C++ の仕様です。とはいっても相当以前から規格が検討されていたこともあり、その仕様のかなりは Visual C++ 2010 や gcc-4.6 で利用することができます。
このドキュメントでも C++11 に関して色々と言及してきましたが、その他の特徴として以下のようなものがあります。
auto
による型推論for
move
と右辺値参照static_assert
詳しくは Wikipedia 等を参考にしてください。