モダン C++ プログラミング
モダン C++ プログラミング
このドキュメントはサイボウズ社内のトレーニング用に作成したものです。
作成時点では C++11 はまだあまり利用できない状況でしたので、C++98 ベースの記述になっています。
いずれ更新を予定しています。
モダンの定義
モダンとはテンプレートメタプログラミング(TMP)を駆使することです。嘘です。
宗教論争に意味はないので、ここでは
「最近の C++ の仕様・機能を理解し、C より実装効率が良く不具合の少ない」
プログラミング技法を「モダン C++ プログラミング」と定義します。
つまり、不具合が少なく、かつ C にはもう戻れなくなるような効率の良さを達成するものです。
学習効率(ROI)が極めて良くないような技法(例えば TMP)は、この定義では除外されます。
勉強方法
お勧めの順序は以下。決して全部を読もうとしないこと。
- C++ Language Tutorial のような、あっさりしたもので基本的な書き方を学ぶ
- Effective C++ を流し読みする。第5章まででよいです。
# つまり継承やテンプレートプログラミングは飛ばしていい - C++ Primer を脇に置いてコーディングする
- それでも分からないときはこのドキュメントやエキスパートに頼る
その他リファレンス
- cplusplus.com
STL のリファレンスが便利 - Cppreference
C++11 にも対応したリファレンス。C++11 でどうなるのか知りたければこちら。
これで十分、いいコードが書けるようになります。
コーディングスタイル
絶対にやったほうがいいことだけまとめておきます。
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; };
- コンストラクタのボディは原則として空
整合性のチェックなどをのぞき、複雑なロジックを避けるべきです。なぜかというと、C++ は Java と違ってオーバーロードされた他のコンストラクタに初期化処理を委譲できないため、複数のコンストラクタで同じ処理を書くことになりがちだからです(後述する C++11 では可能になります)。
デストラクタと継承
継承されるクラスのデストラクタには virtual
つけないといけません。
継承の可能性があるクラス(シングルトンなどは継承されないでしょう)では、virtual
つけるのを原則としましょう。
つけないと、親クラスにアップキャストされたオブジェクトのデストラクタが正常に走りません。
親クラスのデストラクタは自動的に呼び出されるので、サブクラスのデストラクタ内で明示的に呼び出す必要はありません。
class A { public: A() {} virtual ~A() {} // 処理が空でも virtual つける }; class B { public: B() {} ~B() { // no need to call ~A(). } };
C++ で Java のような言語にあるインタフェースが欲しい場合、通常は純粋仮想クラスとして実装します。
struct Interface { virtual void foo() = 0; virtual int bar(const std::string& s) = 0; };
ところが純粋仮想クラスはあくまでクラスであってインタフェースではないため、このようなときも仮想デストラクタを定義するべきです。
struct Interface { virtual ~Interface() {} virtual void foo() = 0; virtual int bar(const std::string& s) = 0; };
名前空間を適切に使う
覚えることは四つ。
- 大域スコープのシンボルは作らない(必ず名前空間を使う)
- ヘッダでは
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 になっているメソッドがあれば警告。
そもそも C++ では子クラスでシグネチャの違うメソッドを 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 }
スコープが明確にできない場合は後述する参照カウント式のスマートポインタ(boost::shared_ptr
等)を使います。スタックオブジェクトを大量に使うとスタックオーバーフローの危険もありますので、大量で長期間生存するオブジェクトはオブジェクトプール等での管理も検討しましょう。
一時オブジェクトの生存期間
上記で「一時オブジェクトはスコープをすぎるとすぐにデストラクタが呼ばれて使えなくなる」と言及しましたが、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 文字列の代わりに std::string
が使われます。
例えば以下のような 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 文字列にしないといけない }
std::string
は可変長のバッファとしても利用できます。その勢いで char 配列としても扱いたいところですが、C++03 規格では string の内部バッファが連続メモリであることは保証していないため、以下のようなコードは動作が保証されていません。C++11 では保証されています。
std::string buf; buf.reserve(256); // 256 byte 確保 char* p = &(buf[0]); // コンパイルは通るし動作もするが、連続の保証はない char* p = buf.data(); // 連続メモリだが const つきなのでコンパイルエラーになる
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()
が呼ばれてプログラムが異常終了してしまうことになります。
というわけで、以下の方針に従ってください。
- 可能な限りデストラクタは何もしないこと
後述するスマートポインタや STL コンテナを使う限り、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; };
後述するルールに従っていれば、コンストラクタでは例外を投げて良いです。
むしろ、投げないのが無理というものです。STLコンテナを使うだけで std::bad_alloc
が飛ぶ可能性があるので。
コンストラクタで例外を投げたときに起こるのは以下のようなことです。
- 初期化済みのメンバーのデストラクタを呼ぶ(初期化前のものは呼ばれません)
- もしオブジェクトが
new
で確保されたものであれば、確保したメモリを解放する - 例外を呼び出し側に propagate する
c.f.
例外指定は使わないこと
Java と違い、例外指定がオプションでかつ実行時チェックである C++ では、例外指定は全く無意味であると結論されています。
C++11 では例外指定は deprecate され、例外を絶対に投げないことを保証する noexcept
キーワードが追加されました。コンパイラの最適化用だと考えてください。
例外の賢い使い方
Java の例外は checked exceptions と呼ばれ、メソッドシグネチャで発生する可能性がある例外を明示する必要があります。
これはなかなかの難物で、多数の例外の列挙を防ぐために Java では一度 catch して別の例外に wrap して投げなおすというコードが多用されます。
C++ の例外は checked exceptions ではないので、情報を付加する目的がないのであれば、wrap するような面倒なことは避けた方が良いでしょう。
詳しくは以下の資料に目を通してください。
例外安全
例外安全とは、例外が発生した後もプログラムが安全に走れる保証のことです。例外安全というと、例外を使わなければ関係ないように聞こえますが、実際にはリソースリークなどを確実に防止するので、長時間走るネットワークサーバーやデーモンでも非常に重要な要素となります。
結論からいえば、例外安全なプログラムは以下のルールに従えば作ることができます。
- STLコンテナを使い、極力
new
しない new
したポインタはスマートポインタに格納する- メモリ以外のリソースは RAII 管理クラスで管理する
- オブジェクトのコピーと代入を制御する
このルールを守ればほとんどの場合例外を安全に使うことができるでしょう。
マルチスレッドプログラム等、現在のスタック(スレッド)以外と協調動作するプログラムでは特別な注意が必要になることがあります。
後日マルチスレッドプログラミングの回で解説します。
RAII
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++ はオブジェクトをコピーや代入するために、通常は単にメモリの値をコピーするコピーコンストラクタや代入オペレーターを自動的に生成してくれます。
ですが、メモリ以外のリソース、例えばロックやファイルはコピーしにくいものです。
以下の方針に従ってコピー・代入を制御してください。
- メモリ上の構造は極力 STL コンテナ(とそのコピー・代入)を使う
- ファイルディスクリプタや同期オブジェクト(mutex 等)があれば、コピー・代入を禁止する
コピー、代入を禁止するには、コピーコンストラクタと代入オペレーターを private 宣言します。
private にアクセスできるメソッドや friend 関数にも禁止するために、宣言のみで定義はしません。
class NonCopyable { public: NonCopyable() {} virtual ~NonCopyable() {} private: mutable mutex_t m_lock; // mutex は普通コピーできない NonCopyable(const NonCopyable& ); // コピーコンストラクタ NonCopyable& operator=(const NonCopyable& ); // 代入オペレータ };
Boost には boost::noncopyable
というクラスがあり、これを private 継承することで簡単にコピー・代入を禁止できます。
また C++11 ではデフォルトのコピーコンストラクタや代入オペレータの生成を禁止することができるようになりました。
class NonCopyable { public: NonCopyable(const NonCopyable&) = delete; // デフォルト生成を禁止 NonCopyable& operator=(const NonCopyable&) = delete; // 同上 ... };
なお、コピーができないオブジェクトを共有するためには、後述する参照カウント式のスマートポインタを利用してください。
スマートポインタの使い方
スマートポインタとは、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
には以下のような制限があります。
- C 配列には利用できない(
delete[]
してくれないため)
C++11 で導入されるstd::unique_ptr
ではできます。C++03 ではstd::vector
を利用しましょう。 - コピーできない
コピーできないため、STLコンテナに入れてはいけません。
STLコンテナに入れるには、後述する参照カウント式のスマートポインタを利用します。
C++11 では std::auto_ptr
は deprecate され、std::unique_ptr
を使うことになりました。
std::unique_ptr
では C 配列も扱えますし、STL コンテナにも入れられます。
std::unique_ptr<char[]> buf( new char[256] ); // char[] の unique_ptr は delete[] される
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コンテナの使い方
STL は今日の C++ コンパイラでは広く実装・最適化されています。
STLの中でも std::string
や std::vector
といった STL コンテナはメモリ資源を RAII で適切に管理してくれる有用なものです。
STLコンテナを使うことで自分で new
, delete
する場面は著しく減らせるでしょう。
ただし、STLコンテナにはいくつか癖があります。
- コピー・代入可能なオブジェクトしか格納できない
例えばconst
なメンバーを持っているオブジェクトは代入できないため、格納できません。
また、auto_ptr
はコピー可能でないため格納できません。
これらを格納する場合は、参照カウント式のスマートポインタを利用してください。 - メモリ連続性の保証がされていないものがあります
C++11 では move
という操作が規定されたため、コピー・代入可能でなくとも move
可能なら STL コンテナに格納できます。
先述した std::unique_ptr
は move
可能なオブジェクトの一例です。
例えば 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++ は仕様が変遷してきたため、ライブラリもその時々のスタイルに合わせて作られています。
ここでは、このドキュメントで定義するモダン C++ に合致するライブラリを紹介します。
具体的には以下の条件を満たすライブラリです。
- 例外安全
- STL の積極的な利用
- マルチスレッドセーフ
POCO
POCO はネットワークアプリケーションを作成するのに必要な機能を揃えたライブラリです。
SSLを含むネットワーク機能に加え、ファイルシステム・スレッド・時刻・ロギング・設定ファイルなどの機能があります。
テンプレートを多用してはいないため、ソースコードはシンプルで読みやすく、コンパイル速度も速いです。
Boost
Boost はテンプレートメタプログラミングを中心とした、非常に高度な C++ の技術を駆使したライブラリです。
C++11 にも多数の仕様が取り込まれる等、広く影響力を持ってはいますが、機能セットとしては POCO と大差がありません。
Boost のプログラムソースは初心者にはとても読めるものではなく、またテンプレートを駆使するためコンパイル速度が非常に遅いです。
特に有用な機能は C++11 に取り込まれていますので、Boost を使うことはお勧めしません。
QT and wxWidgets
QT は GUI アプリケーションの作成で広く使われている C++ のフレームワークです。
ただし、古くからあるため例外安全ではありません。
wxWidgets も以前から使われている GUI ライブラリですが、native GUI ではないのでかっこ悪いのが残念。
例外は使わないが、例外安全ではあります。
その他
この辺参照。
C++11 について
C++11 は 2011 年に規格が標準化された新しい C++ の仕様です。とはいっても相当以前から規格が検討されていたこともあり、その仕様のかなりは Visual C++ 2010 や gcc-4.6 で利用することができます。
このドキュメントでも C++11 に関して色々と言及してきましたが、その他の特徴として以下のようなものがあります。
auto
による型推論- 拡張された
for
move
と右辺値参照static_assert
- スレッドライブラリ
- 正規表現
詳しくは Wikipedia 等を参考にしてください。