2018/03/07

関数プロトタイプ宣言はヘッダファイルとソースファイルのどちらに書くべきか

C言語のコードでは、全ての関数プロトタイプ宣言がヘッダファイルに書いてあったりするものもありますが、 個人的にはヘッダファイルに書くべきものと、ソースファイルに書くべき1ものを明確に区別すべきだと思います。

何をもって区別するのかと言うと、「スコープ」です。 「何を当たり前のことを」と思った方は申し訳ありません。ただ、最近「スコープなんて知らん」と言わんばかりのコードを大量に読むことになってしまいまして。

スコープとは

ある変数名や関数名といった名前を参照できる範囲のこと

スコープ - Wikipedia より

言語によって指定できるスコープには違いがありますが、C言語の関数の場合は publicprivate を区別すべきかと思います2。 C言語に publicprivate という修飾子はないですが、staticextern だと誤解を与える可能性があるので、ここではあえて publicprivate を使います。

public は他のモジュール(ファイル)から参照可能な関数。 private はそのモジュール(ファイル)内でのみ参照可能な関数です。

例えば、特定のハードウェアを監視しているようなモジュールの場合、外から値をチェックするための関数は public ですが、 特定の状態変化を検知して記録するような関数は private になる可能性が高いです3

スコープは狭く

一般的にスコープは狭くしておいた方が安全です。 ハードの状態変化を外から設定できてしまうと、間違ってその関数を別のモジュールから呼んでしまった場合、重大なバグの原因になりえます。

そんなの間違えて呼ばないよ。と思うかもしれません。 自分だけで書いている場合はそうかもしれないですが4、 チームで開発していると誰がどんな勘違いをするかわかりません。

public 関数はヘッダファイルに、private 関数はソースファイルに書く

ヘッダファイルに書かれてる物は、複数のファイルから参照されることを前提にしています5。 つまり、「ヘッダファイルに書いてある」=public と言えます。

先の例だと、以下のように状態を見る関数はヘッダファイルに、状態を記録する関数はソースファイルに書きます。

なお、ソースファイルに書く場合、先行して定義出来る場合はプロトタイプを書かなくても良いでしょう。個人的には書かない方が修正時等に余計な手間が減ってよいと思います。

hoge.h
1
2
3
4
5
6
#ifndef HOGE_H__
#define HOGE_H__
 
extern int hoge_get_state();
 
#endif // HOGE_H__
hoge.c
1
2
3
4
5
6
#include "hoge.h"
 
// このプロトタイプ宣言は書かなくても良い場合もある
static void set_state(int state);
 
// 関数の実体等々

あえて、externstatic を書きました。 上記の定義に従うと、public な関数は externprivate な関数は static になります。

実は、extern なプロトタイプ宣言をソースファイルの中に書いても良いわけですが、それだと各ソースファイルに同じプロトタイプ宣言が散らばることになります。 こういった宣言は一元管理が原則6なので、ヘッダに書いておくべきです。

C++ の場合は public class と private class を区別

C++ でもベターC として使用する場合は原則同じです。

C++ の class を積極的に活用する場合、 class にはアクセス指定子(public private protected)があるので、そちらで関数のスコープ制御を行います。

この時、class 定義をヘッダファイルに書くか、ソースファイルに書くかで、class のスコープを制御するのが良いと思います7

変数はどうする?

一般的に「グローバル変数は使わないほうが良い」というのは広く認識されているのだと思います。

「グローバル変数」とは、どこからも参照可能になっている変数、つまり public な変数です。 さらに言えば、extern 宣言されている変数です。 基本的には「グローバル変数」は使用せず、アクセサを用意する方がより安全だと思います。

とはいえ、「単純に読み書き用のアクセサを用意するのはどうよ?」という考え方も一理あります。 C++ の話ですが、興味深いのでリンクをはっておきます。

参考

C++ - アクセサにするべきかpublicにするべきか?(21279)|teratail

でも、ローカルルールには従いましょう

プロジェクトによっては、 他のファイルから呼んではいけないローカルヘッダを作る等8、独自のルールでスコープを実現している場合もあります。 そういった場合は、ローカルルールに従いましょう。 全体でポリシーが統一されていない方が、後々問題になることが多いように思います。

大事なのは、スコープをわかりやすく提示することです。それにより、誤使用によるバグが減りますし、モジュールの使い方が明確になりやすいです。

もちろん、public にしていても使用に注意が必要な関数があったり、理想通りにはいかないことも多いです。 ただ、そういったことを意識していれば、わかりやすいコメントを入れたり、より具体的な対処が出来るのではないかと思います。

参考書籍

この手のことがどこかに書いてあったはずと思って手持ちの本を探しまわったら、これに書いてありました。

外部に公開する関数、変数のみをヘッダで宣言し、外部に公開する必要のない、その他 の作業用の変数、関数については static 指定子を付けるというのが、C でよく見られるモジュール化の手法で す。

p.72 より

この本では、さらにオブジェクト指向っぽい実装を紹介していて実用的です。 私も 2つ以上のアドレスを持つ I2C モジュールを作成する時等に、この本で紹介されている方法を用いています。

これにも書いてあったかなと思ったのですが、関数のスコープに関してはあまり書かれていませんでした。

しかし、以下の項等は今回の話に通じるところがあります。その他の項目も一読の価値ありです。

  • 9.2 変数のスコープを縮める9
  • 10.6 既存のインターフェースを簡潔にする
  1. もしくは、書く必要さえない 
  2. 変数はもう少し複雑 
  3. わかりにくい例しか思いつかなかった 
  4. 実際は自分で書いたものでも間違えることはよくありますが… 
  5. ローカルヘッダのような、特殊な場合もありますが、一般論として 
  6. 極端な話、ヘッダの内容を全部ソースファイルにコピペすればヘッダファイルはいらないわけですが… 
  7. C++にはローカルクラスもあるので、さらに細かく制御出来る 
  8. 特殊なプレフィクスがついてたりする 
  9. この項目で書かれている 「static メソッド」は Java 等のメンバ変数を汚染しないメソッドのことを指しているので注意 
?

0 件のコメント: