👴🏽

C++ コードをレガシー化させないためのプラクティス

2022/08/29に公開

C++ コードをレガシー化させないためのプラクティス

はじめに

(本記事はもともと 実務でしか身につかないソフトウェア品質開発の話 の一部だったものを再編し独立させたものです。)

C++ は Web のバックエンドからOS開発まで適用できる最も間口の広い言語です。若干下落傾向にありますが、TOIBEのランキング でも初期から5位以上を常にキープしており、ビジネス上のニーズも長期安定しています。特にJavaやPythonでは行えないローレベルな組み込み開発では、当面の間主流であり続けると思われます。

ただ、C++は歴史が古いぶん、洗練された言語とは言えないかもしれません。各々理由があるものの、「抽象クラスのデストラクタはVirtualにする」などの、直感的でない独特の常識がいくつか存在します。それらの多くは、違反してもコンパイルエラーとはならず、実行してみておかしな挙動やリークに悩む、というもので、回避するには多くの前提知識が要求されます。

そのためのノウハウは Effective C++C++ Core Guidelines など既に存在していますが、本記事ではその中から最低限、と思われるポイントをまとめました。

Modern C++

バージョンを問わず "better C" を脱却し C++ らしい実装を行うためのプラクティスです。

ポインタ操作を避ける

ポインタはC言語初心者が躓く最大の山場、と言われていますが、ベテランにとってもポインタにまつわる不具合は発生しがちです。

参照が使える場面では、参照を使いましょう。動的配列を malloc せずに、 std::vector などのコンテナを使いましょう。 固定長配列ならstd::array が使用できます。

ポインタが必要なのは、バイナリ構造を扱うときと、インスタンスを new するとき程度です。例えばフォーマットに基づいてファイル構造をパースしたり、メモリマップド I/O にアクセスする場合などです。

スマートポインタを使う

それでもポインタを使わざるを得ない場面は残っています。例えばメモリサイズの大きなインスタンスをコンテナに格納する場合、値渡しで格納するとソートなどの操作のたびにコピーが発生してしまい、非効率です。

そのような場合は、スマートポインタ(std::unique_ptrstd::shared_ptr) を作って、インスタンスの生存管理を任せましょう。new より std::make_shared などで作成するようにしましょう。

標準ライブラリを使う

残念ながら C++ の標準ライブラリを使うにはテンプレートの知識が必須で、少し難しいです。しかし十分に C++ の恩恵を受けるには乗り越えるべきハードルです。
慣れているから、という理由だけで mallocprintf を使わず、newstd::cout の練習をしましょう。
車輪の再発明を避けるため、これから実装する自前のループが algorithm で提供されていないか、チェックしましょう。

C++11

C++11以降、C++の文法は別言語と思えるほど大きく変わりました。元々大きかった言語仕様が、更に巨大化したわけですが、その変更点の多くは、書きやすさや読みやすさを支援するものですので、概ね歓迎すべき変化です。

ただ初心者向けの教材では新機能について触れることはまれですし、古い教材で学習された方も多いと思いますので、ここで簡単に整理したいと思います。以下にキーワードを挙げますので、正確な詳細はWeb検索してご確認ください。

auto

c++標準ライブラリが提供する機能を使うには、テンプレートを避けて通れませんでしたが、従来の記法では変数定義の際に、その変数の型と初期化に渡すデータを厳密に一致する記述とせねばならず、冗長なだけでなく、不慣れなライブラリなどを使う際には、クラス関係をよく理解していないため、コンパイルを通すことすら難しい場合がありました。

c++11以降で導入されたauto型では初期化データに基づいて自動的に型推論が働くため、記述が簡単になるだけでなく、初期化データの型変更(たとえば int32_t からint64_t への変更)などに対して変数定義側を修正する必要がなかったり、テンプレート関数の戻り値を受け取る際、受け取り変数の型指定を誤ってキャストを発生させてしまう恐れがなくなる、などの利点も持ちます。

初めて本構文を見たとき、他言語における variant 型 のような、何でも保存できる変数を想像したのですがまったく異なり、初期化時に類推された型でずっと固定される、というものです。

ラムダ式

ざっくり言うと、関数内に関数を作れる機能、に近いですが、正確に説明すると、関数オブジェクトを容易に作る機能です。

std::algorithm のソートなどは強力ですが、従来はソート対象の比較方法をカスタマイズするためには、アルゴリズム実行とは別の箇所に比較関数を書く必要があり、可読性を下げていました。

ラムダ式を使えば、同じスコープ内にわかりやすく記述することができます。

それだけでなく、ラムダ式内から式外の変数を取り込む「キャプチャ」という機能もあり、うまく使えば非常に強力です。ただコピーキャプチャはコピーコストが発生しますし、参照キャプチャは参照先の生存期間を意識する必要がありますので、あらかじめ理解が必要です。

range-based for

糖衣構文なので言語上の重大な変更ではないですが、頻出しますので理解が必要です。

従来からコンテナ型の区別(std::vector, std::listなど)と処理の分離の観点から、インデックスカウンタ i, j による for ループではなく、イテレータの利用が推奨されてきました。

しかしイテレータによる記述はインデックスよりも長くなり、読解を妨げるものでした。新しい構文ではPythonのようなシンプルな記述をイテレータで可能としますので、積極的に利用したい構文です。

右辺値参照

unique_ptr などを使わずに、自然な書式でオブジェクトの生存管理責任を移譲するための機能です。

c++プログラマはとにかくコピーコストを嫌いますので、構造体などのオブジェクトを関数から返す際に、ポインタや参照で返すことが一般的でした。これを通常の変数と同様に受け取れながらも、一時オブジェクトがまるごと転送されることで、コピーコストを発生させない機能です。

ただし本構文は正確にコーディングするためには基礎知識が必要で、知らないうちにコピーを発生させてしまう恐れもあります。ライブラリが本構文に対応することで性能向上の恩恵を受けられますが、自分で記述する際には、はじめのうちは無理に右辺値参照を意識せず、スマートポインタなどを使った従来通りの記述でもよいかと考えます。

まとめ

C++ は今なお進化を続ける言語ですが、開発の現場においては常に最新の環境が使用できるとは限りません。

そのような際に上記のプラクティスを守ることで、モダンな、とまでは行かずとも、ある程度コードの腐敗を遅らせることができるはずです。

Discussion