型の相互変換を実装する際に相互インクルードを防ぐ方法

C/C++ で型の相互変換を実装する際に、相互インクルードを防ぐ方法を紹介します。
相互インクルードに陥るパターン
例えば色空間を表す RGB 型と HSV 型を自身で定義し、相互に変換できるように RGB から HSV へ、HSV から RGB への変換関数を実装したとします。
// rgb.hpp
#pragma once
#include "hsv.hpp"
struct rgb
{
int r, g, b;
hsv to_hsv() const
{
return { /* ... */ };
}
};
// hsv.hpp
#pragma once
#include "rgb.hpp"
struct hsv
{
int h, s, v;
rgb to_rgb() const
{
return { /* ... */ };
}
};
// main.cpp
#include "rgb.hpp"
#include "hsv.hpp"
int main()
{
rgb rgb_color = hsv{ 0, 0, 0 }.to_rgb();
hsv hsv_color = rgb{ 0, 0, 0 }.to_hsv();
}
rgb.hpp が hsv.hpp をインクルードし、hsv.hpp が rgb.hpp をインクルードしており、相互インクルード (循環インクルード) の爆誕です。無限にインクルードされ続けるためコンパイルエラーになります。ファイルの依存関係を図示すると次のようになります。
このように型同士の変換を実装する際、愚直に実装すると気づかぬうちに相互インクルードに陥りやすいです。
解消策
型の定義と変換関数の宣言、変換関数の定義に分離すると相互インクルードを防ぐことができます。ファイルの依存関係を見ると分かりやすいので先に示しておきます。
💡宣言と定義の違い
宣言とは「この仕様(引数、戻り値、シンボル名など)の変数や関数が存在する」ことをコンパイラに伝える記述です。定義とは実際の処理内容をコンパイラに伝える記述です。
宣言は何度も行っても問題ありませんが、定義は一度しか行えません。
関数の宣言と定義
void func(); // 宣言 void func() // 定義 { }
型の宣言と定義
struct S; // 宣言 struct S // 定義 { };
メンバ関数の宣言と定義
struct S { void func(); // 宣言 }; void S::func() // 定義 { }
*_fwd.hpp ファイルには型の定義と、変換関数のプロトタイプ宣言を記述します。
ここで重要なのは、変換先の型の宣言さえあれば、変換関数のプロトタイプ宣言を記述できるという点です。つまり変換先の型の定義ファイルをインクルードする必要がなくなります。
// rgb_fwd.hpp
#pragma once
struct hsv; // 変換先の型の宣言
struct rgb
{
int r, g, b;
hsv to_hsv() const; // 変換関数のプロトタイプ宣言
};
// hsv_fwd.hpp
#pragma once
struct rgb;
struct hsv
{
int h, s, v;
rgb to_rgb() const;
};
次に、変換関数の定義部分を実装します。今回は全てヘッダーファイルに実装しますが、ソースファイルに実装しても問題ありません。
※補足ですがヘッダーファイルに関数の定義を書く場合、インライン化して ODR (One Definition Rule) 違反を防ぐ必要があります。もしインライン化しないと、複数のソースファイルで rgb.hpp をインクルードした際に rgb::to_hsv() の定義が複数回出現してしまい、多重定義エラー(リンク時エラー)になります。
// rgb.hpp
#pragma once
#include "hsv_fwd.hpp"
#include "rgb_fwd.hpp"
inline hsv rgb::to_hsv() const
{
return { /* ... */ };
}
// hsv.hpp
#pragma once
#include "rgb_fwd.hpp"
#include "hsv_fwd.hpp"
inline rgb hsv::to_rgb() const
{
return { /* ... */ };
}
main.cpp は変わりません。
// main.cpp
#include "rgb.hpp"
#include "hsv.hpp"
int main()
{
rgb color = hsv{ 0, 0, 0 }.to_rgb().to_hsv().to_rgb().to_hsv().to_rgb();
}
これで相互インクルード防ぎつつ、型の相互変換できるようになりました 💪💪💪
ご覧いただきありがとうございました。