Edited at

C++ ヘッダとソースでファイルを分ける 応用編

More than 1 year has passed since last update.

この記事は C++ Advent Calendar 2016 - Adventar の 2 日目の記事である。

1 日目の記事は C++のつまずきポイント解説 その2 - Qiita を参照。

3 日目の記事は コンパイル中にコンパイルする「コンパイル時Cコンパイラ」をつくった話 - kw-udonの日記 を参照。 3 日目から急に超ハイレベルな内容になっているが わからないならわからないでいい 。いや、よくは無いが怖気づかずに分かる所から始めればよい。

基本編 では、クラスの定義とインラインメンバ関数をヘッダファイルに、その他のメンバ関数をソースファイルに記述する事について書いた。応用編では更にヘッダファイルを細かく分割していく。クラス y の参照を引数として受け取るメンバ関数 f を持つクラス x について考える。


x.hpp

#ifndef   X_HPP

#define X_HPP

#include "y.hpp"

clsas x
{
// variable
private:
int m_value;

// public function
public:
x( int v );
void f( y const& ) const;
};

inline x::x( int const v ) : m_value( v ) {}

#endif // X_HPP



x.cpp

#include <iostream>

#include "x.hpp"

void x::f( y const& arg ) const
{
std::cout << m_value + arg.get() << std::endl;
}


y.hpp

#ifndef   Y_HPP

#define Y_HPP

class y
{
// variable
private:
double m_value;

// public function
public:
y( double v );
double get() const;
};

inline y::y( double const v ) : m_value( v ) {}
inline double y::get() const { return m_value; }

#endif // Y_HPP


関数 x::f は短い関数なのでヘッダファイルに記述してもよいが、ここでは例としてソースファイルに記述した。ここで再び問題になるのは「コンパイル時間の増大」である。

関数や変数の宣言において、使用する型が参照またはポインタであれば、その型の定義が無くとも宣言のみがあれば問題ない。もし定義まで include されるファイルに含まれている場合、対象の型の定義が変更されればその度にコンパイルしなければならない。

そこで、対象の型の宣言のみを別のファイルに記述する。


x.hpp

#ifndef   X_HPP

#define X_HPP

#include "y_fwd.hpp"

clsas x
{
// variable
private:
int m_value;

// public function
public:
x( int v );
void f( y const& ) const;
};

inline x::x( int const v ) : m_value( v ) {}

#endif // X_HPP



x.cpp

#include <iostream>

#include "x.hpp"
#include "y.hpp"

void x::f( y const& arg ) const
{
std::cout << m_value + arg.get() << std::endl;
}


y_fwd.hpp

#ifndef   Y_FWD_HPP

#define Y_FWD_HPP

class y;

#endif // Y_FWD_HPP



y.hpp

#ifndef   Y_HPP

#define Y_HPP

#include "y_fwd.hpp"

class y
{
// variable
private:
double m_value;

// public function
public:
y( double v );
double get() const;
};

inline y::y( double const v ) : m_value( v ) {}
inline double y::get() const { return m_value; }

#endif // Y_HPP


上記のようにする事で y.hpp が変更された時でもそれ自身を include していなければ x.hppinclude しているソースファイルの再コンパイルは走らない。

だが、上記の分割だけではコンパイルエラーになってしまう場合がある。それは互いのメンバ関数が互いのメンバ関数を呼ぶ場合、かつその関数の inline 指定を行う場合である。クラスの定義がなければメンバ関数を呼ぶことは出来ないが、かといってクラスの定義後に相手のヘッダを include するのは不格好であるし、依存関係に対して適切に include が行われるとは限らない。そこでクラスを定義するヘッダファイルとメンバ関数を定義するヘッダファイルを分離する。


x_fwd.hpp

#ifndef   X_FWD_HPP

#define X_FWD_HPP

class x;

#endif // X_FWD_HPP



x_def.hpp

#ifndef   X_DEF_HPP

#define X_DEF_HPP

#include "x_fwd.hpp"
#include "y_fwd.hpp"

class x
{
// variable
private:
int m_value;

// public function
public:
x( int v );
int get() const;
void f( y const& ) const;
};

#endif // X_DEF_HPP



x_inl.hpp

#ifndef   X_INL_HPP

#define X_INL_HPP

#include <iostream>
#include "x_def.hpp"
#include "y.hpp"

inline x::x( int const v ) : m_value( v ) {}
inline int x::get() const { return m_value; }

inline void x::f( y const& arg ) const
{
std::cout << m_value + arg.get() << std::endl;
}

#endif // X_INL_HPP



x.hpp

#ifndef   X_HPP

#define X_HPP

#include "x_fwd.hpp"
#include "x_def.hpp"
#include "x_inl.hpp"

#endif // X_HPP


y_fwd.hpp

#ifndef   Y_FWD_HPP

#define Y_FWD_HPP

class y;

#endif // Y_FWD_HPP



y_def.hpp

#ifndef   Y_DEF_HPP

#define Y_DEF_HPP

#include "y_fwd.hpp"
#include "x_fwd.hpp"

class y
{
// variable
private:
double m_value;

// public function
public:
y( double v );
double get() const;
void f( x const& ) const;
};

#endif // Y_DEF_HPP



y_inl.hpp

#ifndef   Y_INL_HPP

#define Y_INL_HPP

#include <iostream>
#include "y_def.hpp"
#include "x.hpp"

inline y::y( double const v ) : m_value( v ) {}
inline double y::get() const { return m_value; }

inline void y::f( x const& arg ) const
{
std::cout << m_value * arg.get() << std::endl;
}

#endif // Y_INL_HPP



y.hpp

#ifndef   Y_HPP

#define Y_HPP

#include "y_fwd.hpp"
#include "y_def.hpp"
#include "y_inl.hpp"

#endif // Y_HPP

これでお互いのメンバ関数の定義は必用なクラスの定義を参照する事が可能となる。このような複雑な依存関係がなければ x.hppy.hpp を直接 include すればよい。