C++ アドベントカレンダー 3日目。
2日目は お手軽 乱数実装【C++11】
4日目は メンバ関数をもっと分ける
です。
※ タイトル変えました。
せっかく招待いただいたのでなんか書こうと。
なにかすごいことを書かなきゃと気負っていたんだけど、
https://qiita.com/oda-i3/items/0e3ee5707b960fef11a7
を拝読して、気楽になった。
気楽に書こうと思う。
順序に意味はない。思いついた順。
i++
よりも ++i
と書く
i
が int
のような型の場合は
for( int i=0 ; i<LEN ; i++ ){/*略*/}
と書いても
for( int i=0 ; i<LEN ; ++i ){/*略*/}
と書いても全く同じなんだけど、
for( auto && i=cbegin(c); i!=cend(c); ++i ){/*略*/}
のように、i
がなにがしかの iterator だったりする場合は i++
よりも ++i
の方が速いかもしれない(同じかもしれない)。
++i
の方が遅いという可能性はほぼ全く無いので、i++
でも ++i
でもいい場合にはとりあえず ++i
と書いておけば良い。
というわけで、auto x = *(i++);
のように結果の値を利用する場合以外は i++
は避け、++i
を使う。でもまあさっきの例でも auto x=*i;++i;
って2行で書いて ++i
を使いたいけどね。
※ そうすると、go を書いているときにいつもの癖で ++i
と書いてしまってエラーになる。
引数を配列にしない
void foo( int hoge[3] );
のような宣言を見ることがあるけど、この「3」はコンパイラには伝わらないので、このような宣言をするべきではない。
ではどうするべきか。
c++ には std::array
という素晴らしいクラスがあるのでこれを使えば良い:
void foo( std::array<int,3> & hoge ); // hoge を変更する場合
void bar( std::array<int,3> const & hoge ); // hoge を変更しない場合
という具合。これならサイズが 3 以外のものを渡すとコンパイルエラーになる。
std::string
のようなオブジェクトは const 参照で受ける
ポインタのコピーよりもオブジェクトのコピーのほうが高コストになるオブジェクトを関数に渡す際には、const 参照で渡す。
細かいことを気にしなければ。
「組み込み型・ポインタ・イテレータは値渡し。それ以外は全部 参照か const 参照」ぐらいの気持ちでいい。
※ C++ マスター向けの注釈:稀に、std::string
のような型でも値渡しのほうが良い場面があるけどね
--- 追記 ---
コメント欄に書いていただいた通り、C++11 以降であれば 右辺値参照とか ムーブセマンティクス とか呼ばれる受け方もある。
これは概ね、引数のオブジェクトを自分のものにしたい場合に速度向上が見込めるもの。だからいわゆる setter とかでは普通の const 参照と 右辺値参照 の両方を用意するのが良い。
とはいえ、引数として渡されるオブジェクトのクラスが moveコンストラクタを持っていないと意味がない。STL のコンテナや std::string
なんかはみんな moveコンストラクタを持っているので、右辺値参照で受ける関数を書くことには意義がある。
--- ここまで追記 ---
オーバーライドするときは override 修飾子を必ず書く
override
修飾子を書くと、基底クラスの I/F が変わったときにエラーになってくれる。安心。
こんな感じ。
class base{
public:
virtual void foo( std::int64_t x ); // 昔は void foo(std::int32_t); だったけど、変えた。という設定。
};
class derived : public base{
public:
void foo(std::int32_t x) override; // override があるとちゃんとエラーになってくれる。無いとエラーにならない。
};
引数ひとつのコンストラクタはとりあえず explicit
にする。
不用意な型変換を避けるため、引数がひとつのコンストラクタはとりあえず explicit
にする。
explicit
にしないと、以下のようなやや思いがけない暗黙の変換が起こる。
以下の通り:
class foo{
public:
foo( int x ); // 普通は explicit であるべき。
// 略
};
void hoge(foo const & x);
void fuga(){
hoge(123); // foo::foo(int) と hoge(foo const & x); が呼ばれる。
}
このような暗黙の変換が意図しないものであるのなら(大抵意図しないと思う)、explicit
をつける。
逆に explicit
をつけないのなら、なぜつけないのかをコメントで明記すべきと思う。
非メンバの関数をファーストチョイスにする
非メンバで済む関数は、メンバにしない。
private, protected なメンバを参照せずに書けるのなら、メンバにするべきではない。
private っぽくしたいのなら、cpp 内の匿名 namespace に書けばいい。
public にしたいのなら、ヘッダに宣言して、クラスと同じ名前空間に入れる。
protected にしたい場合はまあケースバイケースかな。
クラスの private, protected なメンバを参照している場合のファーストチョイスは、staticメンバ関数。this
を参照しないのなら普通のメンバ関数にすべき理由はない。暗黙の引数 this
を渡さない分ちょっと速くなるかもしれないし(ならないかもしれない)。
念の為に書いておくと、仮想関数は暗黙のうちに this
を参照しているので非メンバにできないよ。
switch〜case はバグの温床
まあとにかく、switch
〜case
はバグの温床だ。
以前。某プロジェクトに急にアサインされて、バグの原因を見つけてくれと言われたときに、とりあえず switch
〜case
を検索したらあっさりバグが見つかった、なんてこともあった。
で。
バグが出にくいようにする方法をいくつか。
switch〜case を書かない
バグの温床なんだから、書かなければいい。
ではどうするか。という問の答え(の一部)が仮想関数による多態。
switch
〜case
をゼロにはできないけど、だいぶ減らせる。
必ず default
を書く
なにもすることがないとしても、 default
を書く。
書くことで、網羅しているよ、ということを読み手に対してアピールすることができる。
もともと存在しなかった default ラベルを実際に書いてみると、これは例外だよなぁという場面だったりすることも多い。
私としては、default ラベルが先頭にあるのがおすすめ。つまり、switch
〜case
を書いたらとりあえず default
を書く。
case ブロックはできるだけ return
で終わる
switch
〜case
はバグの温床なのでできるだけそれ以外のことをしないようにする。
switch
〜case
以外になにもない関数を書いているのであれば、caseブロックは return
で終わることができるはず。
逆に言えば。break
で終わっているような switch
〜case
があるのなら、そのswitch
〜case
は関数に切り出したほうが良い。
だまって fallthrough しない
極めて稀に、break
も return
もしないで次の case に落ちるコードを書きたいことがある。そういう場合は //fallthrough
とコメントに書く。
※ C++17 以降を使っているのなら [[falltough]]
アトリビュートを書く。
そういった対応がなければ、fallthrough はすべてバグとみなす感じがよい。
使えない名前を避ける
よくヘッダファイルが
#if ! defined __HOGE_H__
#define __HOGE_H__
// 中略
#endif
となっているが、はっきり言ってバグである。動いていても、バグはバグ。
C++ では以下の名前は予約されていて(合ってる?)、普通の人は使ってはいけない(たぶん、コンパイラベンダは使っていいので、__FILE__
とか __APPLE__
とかはOK)。
-
_
(下線)+大文字で始まる名前。C言語も。 -
__
(下線2個)で始まる名前。C言語も。 -
__
(下線2個)を含む名前。C++ のみ。 -
_
(下線)で始まる名前。C言語も。ファイルスコープとグローバルスコープのとき。
私は上記のルールを覚えられないので
- 下線で始まったらアウト
- 下線2個を含んでいたらアウト
ぐらいに考えている。
delete と書いたら負け。new も避ける。
delete
があるということは、その前の行とかで 例外が飛んだらリークするということ。
デストラクタでの delete
ならいいかというと、だいたいそうでもなく、スマートポインタを使えば済むことを自力でやってしまっているだけの場合がほとんど。
つまり。独自のコンテナクラスやスマートポインタを実装しているのでもなければ、ソースコード上に delete
を書くべき場面はほぼ無い。
あとよく
class foo{
hoge_t * hoge;
fuga_t * fuga;
public:
foo(){
hoge = new hoge_t;
fuga = new fuga_t;
}
~foo(){
delete fuga;
delete hoge;
}
}
みたいなのを見るけど、だめ。new fuga_t
が例外を出すと delete hoge
が呼ばれないのでリークする。
あと。
new
も、std::make_unique
なんかを使えばほぼ使うべき場面はないわけで、(delete
ほどではないけれど)避けたいところだ。
代入演算子の普通の書き方を知る
代入演算子を書くのは、イディオムを知らないと難しい。
難しいのは、自己代入対策と例外安全。
copy-and-swap で書くと、自己代入対策と例外安全の両者に対処できる。
ムーブ代入は、まだ書いたことがなかったんだけど、下記の通りかなぁ。
こんな感じ:
class hoge
{
// なんか実装。メンバとか。
public:
~hoge();
hoge( hoge const & that );
hoge( hoge && that );
hoge & operator=(hoge const & s)
{
hoge tmp(s); // 例外が飛ぶかもしれないコピーコンストラクタ
swap(tmp); // 例外を投げない swap。
return *this;
}
hoge & operator=(hoge && s)
{
hoge tmp(std::move(s)); // ムーブコンストラクタ
swap(tmp); // 例外を投げない swap。
return *this;
}
void swap (hoge &s) noexcept; // 例外を投げずに swap する
};
これで、コピーコンストラクタが例外を投げてもリークしたりしない。
とはいえ。
自動生成の代入演算子で済むようなクラスにするのが望ましい。
例外は、値を投げて参照で受ける
よく
throw new CHogeException( "hoge!" );
のような例外の投げ方を見るけど、これはだめ。
これは MFC が広めた悪習だとおもうんだけど、どうだろう。
この投げ方だと
- いつ誰が
delete
すべきなのか不明瞭 -
new
に失敗したときに不幸になる
などの問題がある。
というわけで、例外は値で投げて参照で受ける。
値で投げると new
の失敗のようなことが起きない。
値で受けてしまうと スライシングが起きてしまうので、参照で受ける。
具体的にはこんな具合:
throw hoge_exception("hoge!");
try{/*略*/}
catch( hoge_exception & err ){
// do something.
}
Cスタイルキャスト を避ける
Cスタイルキャストってのは
auto p = (foo*)hoge;
の「(foo*)
」みたいなやつ。
Cスタイルじゃないキャストは
auto p = static_cast<foo*>(hoge);
の「static_cast<foo*>(略)
」みたいなやつ。
Cスタイルキャストはいろいろなことがいっぺんに起こってすごくこわい。
そもそもキャストはこわいものなのに、Cスタイルキャストだと怖さが伝わらない。
というわけで、static_cast
、const_cast
、reinterpret_cast
を組み合わせて(ときには dynamic_cast
も使って)書くべきだ。
そのキャストが、型を変えているのか、const を外しているのか、そのあたりがハッキリする。
キャストのつづりが長いのもよい。なんとなく怖さが伝わる。
空っぽのコンストラクタが良いコンストラクタ
コンストラクタの{
と }
の間は空っぽが良い。空っぽなのが良いクラス。
つまり。
foo::foo(){
this->member = x;
}
のように書かず、
foo::foo() : member(x){}
と書こう。初期化子リストを使った後者の方が、たぶん、最適化されやすい。
初期化子リストに書くことで、どのメンバまで構築されたかがコンパイラに伝わり、途中で例外が発生してもちゃんと構築済みのメンバについてのみデストラクタが呼ばれる。
x
の部分が簡単な式にならない場合は、そこを関数として切り出して初期化子リストに入れよう。lambda でもいいけどね。
最新の C++ を使おう
まあそうできない歴史的経緯がある場合があるのは知っているけど、できるだけ新しい C++ を使おう。
新しい機能を使わないとしても、C++11 以降は、C++98 よりも 100倍以上速いことがある のような例もある。
もちろん新しいライブラリや言語機能を使うことで生産性が上がるし、コードの速度も上がりがち。
以上なんだけど
以上なんだけど、たぶんここに書いたことは全部『Effective C++』や『C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス』に載っている。あー switch
〜case
の話は書いてないかな。