72
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C++Advent Calendar 2018

Day 3

C++ ライフをよりハッピーにするための 14の小ネタ

Last updated at Posted at 2018-12-02

C++ アドベントカレンダー 3日目。

2日目は お手軽 乱数実装【C++11】
4日目は メンバ関数をもっと分ける
です。

※ タイトル変えました。

せっかく招待いただいたのでなんか書こうと。
なにかすごいことを書かなきゃと気負っていたんだけど、
https://qiita.com/oda-i3/items/0e3ee5707b960fef11a7
を拝読して、気楽になった。

気楽に書こうと思う。

順序に意味はない。思いついた順。

i++ よりも ++i と書く

iint のような型の場合は

c++
for( int i=0 ; i<LEN ; i++ ){/*略*/}

と書いても

c++
for( int i=0 ; i<LEN ; ++i ){/*略*/}

と書いても全く同じなんだけど、

c++
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 と書いてしまってエラーになる。

引数を配列にしない

c++
void foo( int hoge[3] );

のような宣言を見ることがあるけど、この「3」はコンパイラには伝わらないので、このような宣言をするべきではない。

ではどうするべきか。

c++ には std::array という素晴らしいクラスがあるのでこれを使えば良い:

c++11
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 が変わったときにエラーになってくれる。安心。

こんな感じ。

c++11_or_later
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 にしないと、以下のようなやや思いがけない暗黙の変換が起こる。
以下の通り:

c++
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 はバグの温床

まあとにかく、switchcase はバグの温床だ。
以前。某プロジェクトに急にアサインされて、バグの原因を見つけてくれと言われたときに、とりあえず switchcase を検索したらあっさりバグが見つかった、なんてこともあった。

で。

バグが出にくいようにする方法をいくつか。

switch〜case を書かない

バグの温床なんだから、書かなければいい。
ではどうするか。という問の答え(の一部)が仮想関数による多態。
switchcase をゼロにはできないけど、だいぶ減らせる。

必ず default を書く

なにもすることがないとしても、 default を書く。
書くことで、網羅しているよ、ということを読み手に対してアピールすることができる。

もともと存在しなかった default ラベルを実際に書いてみると、これは例外だよなぁという場面だったりすることも多い。

私としては、default ラベルが先頭にあるのがおすすめ。つまり、switchcase を書いたらとりあえず default を書く。

case ブロックはできるだけ return で終わる

switchcase はバグの温床なのでできるだけそれ以外のことをしないようにする。
switchcase 以外になにもない関数を書いているのであれば、caseブロックは return で終わることができるはず。
逆に言えば。break で終わっているような switchcase があるのなら、そのswitchcase は関数に切り出したほうが良い。

だまって fallthrough しない

極めて稀に、breakreturn もしないで次の case に落ちるコードを書きたいことがある。そういう場合は //fallthrough とコメントに書く。
※ C++17 以降を使っているのなら [[falltough]] アトリビュートを書く。

そういった対応がなければ、fallthrough はすべてバグとみなす感じがよい。

使えない名前を避ける

よくヘッダファイルが

c++
#if ! defined __HOGE_H__
#define __HOGE_H__
// 中略
#endif

となっているが、はっきり言ってバグである。動いていても、バグはバグ。

C++ では以下の名前は予約されていて(合ってる?)、普通の人は使ってはいけない(たぶん、コンパイラベンダは使っていいので、__FILE__ とか __APPLE__ とかはOK)。

  • _(下線)+大文字で始まる名前。C言語も。
  • __(下線2個)で始まる名前。C言語も。
  • __(下線2個)を含む名前。C++ のみ。
  • _(下線)で始まる名前。C言語も。ファイルスコープとグローバルスコープのとき。

私は上記のルールを覚えられないので

  • 下線で始まったらアウト
  • 下線2個を含んでいたらアウト

ぐらいに考えている。

delete と書いたら負け。new も避ける。

delete があるということは、その前の行とかで 例外が飛んだらリークするということ。
デストラクタでの delete ならいいかというと、だいたいそうでもなく、スマートポインタを使えば済むことを自力でやってしまっているだけの場合がほとんど。

つまり。独自のコンテナクラスやスマートポインタを実装しているのでもなければ、ソースコード上に delete を書くべき場面はほぼ無い。

あとよく

c++
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 で書くと、自己代入対策と例外安全の両者に対処できる。
ムーブ代入は、まだ書いたことがなかったんだけど、下記の通りかなぁ。

こんな感じ:

c++11
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 する
};

これで、コピーコンストラクタが例外を投げてもリークしたりしない。

とはいえ。
自動生成の代入演算子で済むようなクラスにするのが望ましい。

例外は、値を投げて参照で受ける

よく

c++
throw new CHogeException( "hoge!" );

のような例外の投げ方を見るけど、これはだめ。
これは MFC が広めた悪習だとおもうんだけど、どうだろう。
この投げ方だと

  • いつ誰が delete すべきなのか不明瞭
  • new に失敗したときに不幸になる

などの問題がある。

というわけで、例外は値で投げて参照で受ける。
値で投げると new の失敗のようなことが起きない。
値で受けてしまうと スライシングが起きてしまうので、参照で受ける。

具体的にはこんな具合:

c++
throw hoge_exception("hoge!");
c++
try{/*略*/}
catch( hoge_exception & err ){
  // do something.
}

Cスタイルキャスト を避ける

Cスタイルキャストってのは

c++
auto p = (foo*)hoge;

の「(foo*)」みたいなやつ。

Cスタイルじゃないキャストは

c++
auto p = static_cast<foo*>(hoge);

の「static_cast<foo*>(略)」みたいなやつ。

Cスタイルキャストはいろいろなことがいっぺんに起こってすごくこわい。
そもそもキャストはこわいものなのに、Cスタイルキャストだと怖さが伝わらない。

というわけで、static_castconst_castreinterpret_cast を組み合わせて(ときには dynamic_cast も使って)書くべきだ。

そのキャストが、型を変えているのか、const を外しているのか、そのあたりがハッキリする。
キャストのつづりが長いのもよい。なんとなく怖さが伝わる。

空っぽのコンストラクタが良いコンストラクタ

コンストラクタの{}の間は空っぽが良い。空っぽなのが良いクラス。

つまり。

c++
foo::foo(){
  this->member = x;
}

のように書かず、

c++
foo::foo() : member(x){}

と書こう。初期化子リストを使った後者の方が、たぶん、最適化されやすい。
初期化子リストに書くことで、どのメンバまで構築されたかがコンパイラに伝わり、途中で例外が発生してもちゃんと構築済みのメンバについてのみデストラクタが呼ばれる。

x の部分が簡単な式にならない場合は、そこを関数として切り出して初期化子リストに入れよう。lambda でもいいけどね。

最新の C++ を使おう

まあそうできない歴史的経緯がある場合があるのは知っているけど、できるだけ新しい C++ を使おう。

新しい機能を使わないとしても、C++11 以降は、C++98 よりも 100倍以上速いことがある のような例もある。

もちろん新しいライブラリや言語機能を使うことで生産性が上がるし、コードの速度も上がりがち。

以上なんだけど

以上なんだけど、たぶんここに書いたことは全部『Effective C++』や『C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス』に載っている。あー switchcase の話は書いてないかな。

72
52
14

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?