Help us understand the problem. What is going on with this article?

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

PR: CADDiではバックエンドエンジニア、フロントエンジニア、アルゴリズムエンジニア、SRE等などを募集しています

この記事は 初心者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 。 

agate-pris
C++2a未対応。
https://agate-pris.dev
caddi
製造業の受発注プラットフォーム「CADDi」を提供しています。 モノづくりに携わるすべての人が、本来持っている力を最大限に発揮できる社会を実現する。産業の常識を変える「新たな仕組み」をつくります。 「CADDi」は金属加工品のCAD・設計図の解析から複雑な物流を表現するUIまで幅広い開発をしており、常に開発環境に最新の技術をとり入れて、より良いプロダクトを作るように心がけております。
https://corp.caddi.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away