1. 導入
C++で開発を行っていると、外部ライブラリ(FFI: Foreign Function Interface)との連携や、C言語で書かれたレガシーコードとの接続が避けて通れない場面があります。その際、単に「データを渡せば動く」と考えてはいけません。型が「Trivially Copyable(トリビアルにコピー可能)」であるかどうかを意識しないと、予期せぬABI(Application Binary Interface)の不一致や、不要なメモリコピーによるパフォーマンス低下を招きます。本稿では、なぜC++のオブジェクトモデルがABIに影響を与えるのか、その仕組みを解説します。
2. 基礎知識
C++において、型が「Trivially Copyable」であるとは、そのオブジェクトをメモリ上のバイト列として直接コピーしても安全であることを意味します。具体的には、以下の条件を満たす必要があります。
・ユーザー定義のコピーコンストラクタやコピー代入演算子を持たない
・仮想関数(virtual)を持たない
・仮想基底クラスを持たない
C言語には「クラス」という概念がなく、構造体は単なるメモリの塊として扱われます。もしC++側でコンストラクタや仮想関数を持つクラスを定義すると、コンパイラは「隠れたメンバー(vptrなど)」を付与したり、特殊な初期化処理を挿入したりします。この結果、C言語側が期待するメモリレイアウトと乖離が生じ、相互運用が不可能になります。
3. 実装/解決策
C言語との互換性を維持するためには、構造体を「POD(Plain Old Data)」、あるいはC++11以降の標準に従った「Trivially Copyable」な型として定義する必要があります。
・ロジック:
外部ライブラリと共有する構造体には、メソッドやコンストラクタを一切含めない「Plainな構造体」として定義し、ロジックが必要な場合は別途非メンバー関数を作成してください。また、`std::is_trivially_copyable
4. サンプルプログラム
include
include
include
// C言語と互換性のある構造体(Trivially Copyable)
struct RawData {
uint32_t id;
float val;
};
// C言語と互換性のない構造体(仮想関数を持つためvptrが必要)
struct BadData {
uint32_t id;
virtual void foo() {}
};
void check_compatibility() {
// コンパイル時に型がトリビアルであるか検証する
static_assert(std::is_trivially_copyable
std::cout << "RawDataのサイズ: " << sizeof(RawData) << " bytes" << std::endl; // BadDataはvptrを持つため、通常の構造体よりサイズが大きくなる可能性がある std::cout << "BadDataのサイズ: " << sizeof(BadData) << " bytes" << std::endl; } int main() { check_compatibility(); return 0; }
5. 応用・注意点
非トリビアルな型を引数として渡すと、コンパイラはABIの仕様に従い、レジスタ渡しではなく「スタック上の隠れたメモリ領域」を介してポインタを渡す処理(Hidden Pointer)を自動的に挿入します。これは、関数呼び出しのたびにスタック操作とメモリコピーが発生することを意味し、特に高頻度で呼ばれる計算関数ではボトルネックとなります。
現場での注意点:
1. ABIの差異: コンパイラや環境(System V ABI vs Windows x64 ABI)によって「レジスタ渡し」のルールが異なります。外部連携が前提の型には、極力プリミティブ型のみを含めるようにしてください。
2. パディング: 構造体のメンバ順序やアライメントによってパディング(隙間)が挿入されます。`#pragma pack` などで制御することは可能ですが、パフォーマンス低下を招くため、可能な限りメンバの配置を最適化する(サイズの大きい順に並べるなど)のが定石です。
3. `extern “C”`: 外部ライブラリとのインターフェース関数には必ず `extern “C”` を付与し、C++の関数名マングリングを無効化してください。

コメント