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

  • 10
    いいね
  • 3
    コメント

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

1 日目の記事は Push_backとEmplace_back - Qiita を参照。

応用編は C++ ヘッダとソースでファイルを分ける 応用編 - Qiita を参照。

閑話休題。 C++ では、クラスの定義とそのメンバ関数の定義とを、ヘッダファイルとソースファイルとで分割するのが一般的である。

c.hpp
#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 に分割する。

c.hpp
#ifndef   C_HPP
#define   C_HPP

class c
{
    // variable
    private:
    int m_value;

    // accessor
    public:
    int get();
    void set( int value );
};

#endif // C_HPP
c.cpp
#include "c.hpp"

int c::get()
{
    return m_value;
}

void c::set( int const value )
{
    m_value = value;
}

ヘッダファイルの見た目がグッとシンプルになった。クラス c のメンバが一目瞭然である。 1

しかしながら、上記のコードには「動作が遅い」という欠点がある。メンバ関数 get 及び set の内容は極めて短く、通常の関数呼び出しでは実行時のコストが勿体無い。有り体に言えば、 C の構造体のように、直接アクセスした方が高速である、という事だ。この欠点を解消するため、関数の定義を再度ヘッダファイルに戻す。

c.hpp
#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

応用編は こちら


  1. c.hppc.cpp とでメンバ関数 set の引数の型が異なっているように見えるが問題無い。引数の型が参照型ではなく値型であれば、関数の定義側で引数の型に const を付けるか否かは自由である。これは関数のシグネチャが同じだからである。初心者向けとは言い難いが、 C++のconstメンバ関数の挙動を直感的に理解する - Qiita が参考になる。ここでは、クラスの定義側ではコードの見た目がシンプルになる様 const を付けず、メンバ関数の定義側では値が不変である事を明示するために const を付けた。 

  2. inline 指定子がついている場合でも、必ずしも inline 展開されるとは限らない。実際に展開するか否かはコンパイラが決定する。コンパイラのビルドオプションによって、それらの条件をある程度制御可能な場合もある。 

  3. include は基本的に指定されたファイルの内容を記述された箇所にそのまま展開する指令である。よって複数のファイルからヘッダファイルが include された場合、メンバ関数の定義が複数箇所に記述されている事になり C++ の単一定義の原則に抵触する。単一定義の原則は One Definition Rule の対訳であり、しばしば ODR と略される。また、これに抵触する事を ODR 違反とも呼ぶ。ここではメンバ関数に inline 指定子を付けることで、 ODR 違反を回避する事が出来る。 

  4. クラステンプレートのメンバ関数は、長い関数であってもヘッダに定義を記述する。クラステンプレートのメンバ関数及び、関数テンプレートは inline 指定子を付けずとも基本的に ODR 違反とはならない。 inline 指定子はコンパイラに対する展開指示であるので、テンプレートであっても展開されて欲しい場合には inline を付けてよい。また、ビルドせずとも外部から使用できるようにするため、あえて全ての実装をヘッダに書く場合もある。このようなライブラリはしばしばヘッダオンリー等と呼ばれる。 

  5. 内部結合、外部結合、 static 指定子、 extern 指定子、 etc 。