PR: CADDiではバックエンドエンジニア、フロントエンジニア、アルゴリズムエンジニア、SRE等などを募集しています。
この記事は 初心者C++er Advent Calendar 2016 - Adventar の 2 日目の記事である。
1 日目の記事は Push_backとEmplace_back - Qiita を参照。
応用編は C++ ヘッダとソースでファイルを分ける 応用編 - Qiita を参照。
閑話休題。 C++ では、クラスの定義とそのメンバ関数の定義とを、ヘッダファイルとソースファイルとで分割するのが一般的である。
#ifndef C_HPP
#define C_HPP
class c
{
// variable
private:
int m_value;
// acsessor
public:
int get()
{
return m_value;
}
void set( int const value )
{
m_value = value;
}
};
#endif // C_HPP
上記はファイルを分割していない例だ。クラスの定義の中に、関数の定義まで全て記述しているため、ゴチャゴチャとしてしまっている。まずはこれらをヘッダファイル c.hpp
とソースファイル c.cpp
に分割する。
#ifndef C_HPP
#define C_HPP
class c
{
// variable
private:
int m_value;
// accessor
public:
int get();
void set( int value );
};
#endif // C_HPP
#include "c.hpp"
int c::get()
{
return m_value;
}
void c::set( int const value )
{
m_value = value;
}
ヘッダファイルの見た目がグッとシンプルになった。クラス c
のメンバが一目瞭然である。 1
しかしながら、上記のコードには「動作が遅い」という欠点がある。メンバ関数 get
及び set
の内容は極めて短く、通常の関数呼び出しでは実行時のコストが勿体無い。有り体に言えば、 C の構造体のように、直接アクセスした方が高速である、という事だ。この欠点を解消するため、関数の定義を再度ヘッダファイルに戻す。
#ifndef C_HPP
#define C_HPP
class c
{
// variable
private:
int m_value;
// accessor
public:
int get();
void set( int value );
};
inline int c::get()
{
return m_value;
}
inline void c::set( int const value )
{
m_value = value;
}
#endif // C_HPP
ファイルの前半でクラスの定義を行っているが、この時メンバ関数は宣言のみを行い定義は行わない。メンバ関数の定義はクラスの定義の完了後、 inline
指定子をつけて行う。 inline
指定子は、メンバ関数を呼び出した時、その処理内容を呼び出し箇所に展開する様に指示を行う。 これにより、多くの環境において 2 データメンバに直接アクセスした場合と同等の処理速度が得られる。また inline
指定子を付けなかった場合、複数のファイルでヘッダファイルを include
していた場合はエラーとなる。 3
極短い関数であれば、高速化のためにヘッダ側にメンバ関数の定義を記述する事が望ましいが、長い関数はそもそも通常 inline
展開が行われない。それだけでなく、ヘッダファイルに変更が加えられた時、そのヘッダを include
している全てのソースファイルの再コンパイルが行われるため、頻繁に変更を行う事が予想される部分をヘッダ側に記述するとコンパイル時間を徒に増大させてしまう。よって、基本的に長い関数はヘッダファイルではなくソースファイルにに記述すべきである。 4
最後に注意点を述べる。本記事で述べた inline
の用法は C++ における用法である。 inline
指定子は C でも利用する事が出来る。当然 C ではクラスの構文は存在しないため、フリー関数での利用になるが C と C++ とでは挙動が異なるため注意深く利用する必用がある。 5
応用編は こちら 。
-
c.hpp
とc.cpp
とでメンバ関数set
の引数の型が異なっているように見えるが問題無い。引数の型が参照型ではなく値型であれば、関数の定義側で引数の型にconst
を付けるか否かは自由である。これは関数のシグネチャが同じだからである。初心者向けとは言い難いが、 C++のconstメンバ関数の挙動を直感的に理解する - Qiita が参考になる。ここでは、クラスの定義側ではコードの見た目がシンプルになる様const
を付けず、メンバ関数の定義側では値が不変である事を明示するためにconst
を付けた。 ↩ -
inline
指定子がついている場合でも、必ずしもinline
展開されるとは限らない。実際に展開するか否かはコンパイラが決定する。コンパイラのビルドオプションによって、それらの条件をある程度制御可能な場合もある。 ↩ -
include
は基本的に指定されたファイルの内容を記述された箇所にそのまま展開する指令である。よって複数のファイルからヘッダファイルがinclude
された場合、メンバ関数の定義が複数箇所に記述されている事になり C++ の単一定義の原則に抵触する。単一定義の原則は One Definition Rule の対訳であり、しばしば ODR と略される。また、これに抵触する事を ODR 違反とも呼ぶ。ここではメンバ関数にinline
指定子を付けることで、 ODR 違反を回避する事が出来る。 ↩ -
クラステンプレートのメンバ関数は、長い関数であってもヘッダに定義を記述する。クラステンプレートのメンバ関数及び、関数テンプレートは
inline
指定子を付けずとも基本的に ODR 違反とはならない。inline
指定子はコンパイラに対する展開指示であるので、テンプレートであっても展開されて欲しい場合にはinline
を付けてよい。また、ビルドせずとも外部から使用できるようにするため、あえて全ての実装をヘッダに書く場合もある。このようなライブラリはしばしばヘッダオンリー等と呼ばれる。 ↩ -
内部結合、外部結合、
static
指定子、extern
指定子、 etc 。 ↩