C言語で書かれたソースコードをC++化する作業に関わったので、メモを残しておく。
前提条件
- 主にC++やC言語で構成されたアプリケーションにおいて、中間層に位置するコンポーネントの1つとして、C言語で書かれたライブラリがある。
- このライブラリは今後も積極的に手が加えられることが見込まれるが、さすがにC言語では色々と厳しくなってきたので、C++に移行する。
- このコンポーネントを挟む上位層も下位層もC言語ないしC++で書かれている。現状ではC/C++以外の言語を選択することは適切ではないと判断した。
- C++化の初期段階として、まずは公開インタフェースは既存の(C言語の)ままに、内部実装を「Better C」化することにした。
- つまり、ヘッダファイルには極力手を入れず、ソースファイルを弄っていく。
- 余裕があれば、公開インタフェースについても多少のC++化を行うかもしれない。
- C++の言語仕様としてはC++20以降を想定する。
作業の流れ
- 最低限のC++化(C言語からC++への置き換え)
- 機械的に対応可能な「Better C」化
- 必要に応じて:
- より高度なC++化
- 公開インタフェースのC++化
最低限のC++化(C言語からC++への置き換え)
まずはBetter C化を行うための土台作りから。
- ソースファイルのサフィックス(拡張子)を
.cから.cppに変更する。同時に、使用しているビルドシステムに登録されているソースファイル名のサフィックスも変更する。- サフィックスについては、対象プロジェクトに応じて
.cpp以外の形式も検討する。 - 興味深いことに、伝統的な
make(1)を用いるプロジェクトにおいては、Makefileの書き方次第では「ソースファイル名の変更に伴う対応」が不要な場合もある。
- サフィックスについては、対象プロジェクトに応じて
- 公開インタフェースについて、外部にCリンケージとして見せるようにする。
- ヘッダファイルで公開している関数・変数の宣言に
extern "C"を付与する。 - ソースファイル中の公開関数・グローバル変数の定義に
extern "C"を付与する。
- ヘッダファイルで公開している関数・変数の宣言に
- ソースコード内のインクルードディレクティブについて、必要に応じて
extern "C"を付与する。- C言語で書かれたライブラリのヘッダファイルの中には、時々「C言語以外で書かれたソースファイルに取り込まれること」を想定していないものがある。
- この段階で一度、C++コンパイラでコンパイルしてみる。
- コンパイルエラーが発生した場合には、該当する部分を確認して修正する。C言語からC++に移行する場合には、次のような点が問題となりやすい。
- ポインタ型の型変換。型チェックの強化にともない、C言語ではセーフだった「ポインタ型から、別のポインタ型への変換」まわりでエラーとなることが意外とある。
- 整数型から列挙型への型変換。型チェックの強化により、明示的なキャストが必要となった。
- 型定義のネスト。構造体の定義中で別の構造体/共用体/列挙体を定義している場合、C言語とC++では名前のスコープが異なることが原因でコンパイルエラーとなることがある。
- エラーにはならないが警告がでることもある。この場合も、該当する部分を確認して修正する。
- 型チェックまわりで警告がでるようになることが多い、という印象がある。
- コンパイルエラーが発生した場合には、該当する部分を確認して修正する。C言語からC++に移行する場合には、次のような点が問題となりやすい。
- コンパイルエラーや警告メッセージへの対応を行うことで、最低限の「C言語からC++への置き換え」が完了する。
機械的に対応可能な「Better C」化
以下の対応はソースファイルにたいしてのみ実施する(外部には引き続き「C言語時代のAPI」をそのまま公開するため、ヘッダファイルはBetter C化しない)。
- インクルードしている標準ライブラリのヘッダファイル名をC++のものに置き換える。
- 例えば
#include <string.h>を#include <cstring>に置き換える、みたいなこと。
- 例えば
NULLマクロからnullptrキーワードに置き換える。climitsやcstdintなどに定義されていマクロ定数からstd::numeric_limitsの静的メンバ関数への置き換えを検討する。- 不要な
typedefを削除する。- 例えば
struct FooをFooとだけ記述するためにtypedef struct Foo Foo;と定義することは、C++では不要である。構造体だけでなく、共用体や列挙体でも同じくtypedefは不要となる。
- 例えば
typedefから「usingを使用した型エイリアス」に移行する。- 構造体と共用体の定義に
final指定子を付けて派生を禁止する。- C言語時代に定義された構造体や共用体は、派生元となることを想定した実装になっていないので、ひとまず明示的に派生を禁止しておく。
- 後で必要に応じて
finalを止めて派生を解禁していく。
- 関数の宣言・定義に
noexceptキーワードを付けて「例外を送出しない」ことを明示する。- C言語時代に定義された関数が例外を送出することはない……はず。なのでひとまず明示しておく。
- C++化を進めていく過程で例外が発生するようになった場合は:
- コンポーネント内部の非公開関数では、例外を送出する可能性が出てきた関数から
noexceptキーワードを削除する。 - コンポーネントの外部に公開される関数では、
try-catchを使って例外が外に出ないようにしつつ、何かしら問題が発生したことを別の方法(例えば戻り値のエラーコードなど)で通知するようにする。なぜならば、元々C言語で実装されていたコンポーネントを使う側では、例外が送出されることを想定していない(だってC言語だよ?)可能性が高いからである。
- コンポーネント内部の非公開関数では、例外を送出する可能性が出てきた関数から
- 関数の定義にて、ビルド時に未使用の引数についての警告が出力されないように
(void) foo;みたいなことをしているケースにおいて、引数リストから仮引数の名前を削除する(型名は残す)方法に置き換えることを検討する。- C++では合法である。
- 非公開関数やファイルスコープ変数を無名名前空間に移動した上で、static指定子を削除する。
- 明示的な型キャストをCスタイルからC++スタイルに置き換える。
- 定数の定義をプリプロセッサ(マクロ)からconstexprに置き換える。
- 列挙体に基底の整数型を指定することを検討する。
enum classじゃない従来式のenumでも型指定できる。
- 従来の列挙体から
enum class(スコープを持つ列挙体)への置き換えを検討する。 - ローカル変数を宣言している部分について、型名から
autoへの置き換えを検討する。 - 非公開関数の引数について、ポインタから参照への置き換えを検討する。
- (C99よりも前のコードベースの場合)独自のブーリアン型から
boolへの置き換えを検討する。 - (優先度:低)コメントをCスタイルからC++スタイルに置き換える。
- 1行コメントなどは正規表現で変換しやすい。
- 装飾を伴うヘッダコメントの類は機械的な変換が難しいので、あえて対応せず既存のままとする。
より高度なC++化
ここから先は、機械的な対応が難しく、少し大掛かりな変更となるので、慎重に進める。引き続きソースファイルにたいしてのみ実施する。
- POD (Plain Old Data) でなくても構わない構造体・共用体について:
memset(3)によるゼロ埋めから、コンストラクタ呼び出し時の初期化(非静的メンバ変数の初期化も含む)への置き換えを検討する。- メンバ変数の値を初期値に戻すようなメンバ関数(例えば
clearやresetみたいなやつ)を定義して使用する、でもよい。
- メンバ変数の値を初期値に戻すようなメンバ関数(例えば
- 比較演算などの特定の処理について、関数呼び出しから演算子オーバーロードへの置き換えを検討する。
algorithmなどの標準ライブラリの機能と組み合わせやすくなる。
- 一時的な用途の複合型として構造体を定義して使用している部分について、
std::tupleやstd::pairなどへの置き換えを検討する。 - C言語文字列から
std::stringやstd::string_viewへの置き換えを検討する。- リテラル構文についても検討する。
- 組込み配列から
std::arrayなどのコンテナへの置き換えを検討する。 - 従来の
malloc(3)/free(3)でオブジェクトを管理している部分について、スマートポインタの導入を検討する。 - スレッド間の同期用プリミティブとして「
volatileな整数型」を使っている部分をstd::atomicに置き換える。- 必要に応じて「適切なメモリオーダー」を指定することも検討する。
- 「任意の型のオブジェクトへの参照」を引き渡すために
void *を用いている部分について、std::anyやstd::variantへの置き換えを検討する。 - スコープを超えて「任意の型のオブジェクト」を引き渡すために
void *と動的メモリを用いている部分について、std::anyやstd::variantを用いて実体を引き渡すことを検討する。 - 関数ポインタから関数オブジェクトへの置き換えを検討する。
- 関数ポインタ型から
std::functionに置き換える。 - 関数を定義するのではなく、ラムダ式を用いてインラインに定義する。
- 関数ポインタ型から
- ブロックスコープを用いて「関数内のごく一部で使用する変数」を定義して使用している部分について、状況次第でif・switch・範囲forの初期化式を用いる方法への置き換えを検討する。
- 従来のfor文を用いてシーケンスを辿っているコードについて、以下への置き換えを検討する。
- 標準ライブラリの
algorithmの機能。 - 標準ライブラリの
numericの機能。 - 範囲for
- 標準ライブラリの
- 論理的に同じ機能を提供する関数群があり、関数名の衝突を避けるために異なる名前を付けていた場合に:
- 関数の多重定義(オーバーロード)を利用して、関数名を統一することを検討する。
- 関数内の式・文の論理構造まで同一(異なるのはデータ型だけ)である場合には、関数テンプレートを利用して、関数の実装を含めて統一することを検討する。
- 関数形式マクロからインライン関数や関数テンプレートへの置き換えを検討する。
- 独自実装の機能から、標準ライブラリで提供されている類似機能への置き換えを検討する。
- 環境依存の機能から、標準ライブラリで提供されている類似機能への置き換えを検討する。
- 例えばスレッド関連、正規表現、疑似乱数など。
公開インタフェースのC++化
C言語のコードから利用されることが無ければ、公開インタフェースをC++化してもよいだろう。
外部への影響が少ない変更
- インクルードしている標準ライブラリのヘッダファイル名をC++のものに置き換える。
- 公開インタフェースをC++リンケージ化する。
- 不要な
typedefを削除する。 typedefから「usingを使用した型エイリアス」に移行する。- 構造体と共用体の定義に
final指定子を付けて派生を禁止する。 - 定数の定義をプリプロセッサ(マクロ)からconstexprに置き換える。
- 列挙体に基底の整数型を指定することを検討する。
外部への影響が避けられない小変更
- モジュール名をプレフィックスとして用いている場合に、名前空間の利用を検討する。
- 例えば
foo_Initializeならfoo::Initialize。
- 例えば
- 従来の列挙体から
enum class(スコープを持つ列挙体)への置き換えを検討する。 - (C99よりも前のコードベースの場合)独自のブーリアン型から
boolへの置き換えを検討する。 - POD (Plain Old Data) でなくても構わない構造体・共用体について、コンストラクタの実装を検討する。メンバ変数の値を初期値に戻すようなメンバ関数でもよい。
- 関数ポインタから関数オブジェクトへの置き換えを検討する。
- 関数の引数について、ポインタから参照への置き換えを検討する。
その他
元々の「C言語で実装した際の基本設計・基本構造」を保ちながらC++化するため、局所的にはC++らしいコードになるものの、全体としてはBetter Cな感じになる。
やはり最初からC++ありきで設計した場合とは違ってくるよね。