Using extern inline - ytomino/headmaster GitHub Wiki

extern inline をどう使うか、という覚え書きです。

Contents

inline関数は、C言語ではC99で正式に採用されました。 それまでは各コンパイラの独自実装、またはC++のinlineがC言語でも使える、といったものでしたが、C99のもと統一されました。

inline関数にも種類がありまして、storage-classとの組み合わせで3種類あります。

  • static inline
  • inline
  • extern inline

意味としては普通のstatic関数と変わりません。 inline展開しろというヒントをコンパイラに与えてはいますが、別にヒントなんか無くてもコンパイラはstatic関数は最適化の過程で勝手に展開したりその結果参照されなくなったシンボルを消したりしますので、事実上気分の問題です。

一番、細かい事を考えずに使えますが、ヘッダファイル中で定義して、もしinline展開されなかった場合、そのコードの実体は各オブジェクトファイルに重複して残る事になります。

常に、実体は生成されません。 inline展開するか、外部シンボルを呼び出すか、どちらかになります。

インライン展開はしたいが、関数のアドレスも取りたい、という時に、マクロを使った次のようなテクニックがあります。

int DOUBLE(int x);
#define DOUBLE(x) ((x) * 2)

DOUBLE の後に括弧を続けた場合はプリプロセッサによってマクロが展開されて計算式になり、括弧が無い場合はマクロは展開されずに DOUBLE のまま関数のアドレスになります。

C99の inline は、マクロを使わずにこのテクニックを再現できます。 勿論、どこかで inline ではない実体を定義してやらないと、シンボルが見つからずにリンクでこけます。

常に、実体が生成されます。 inline展開するか、同じオブジェクトファイルにある実体を呼び出すか、どちらかになります。

extern の意味からすると反対と思いません? 私は思います。 しかし、規格 (6.7.4 Function specifiers) の例を見て納得しました。

inline double fahr(double t) {
  return (9.0 * t) / 5.0 + 32.0;
}
inline double cels(double t) {
  return (5.0 * (t - 32.0)) / 9.0;
}
extern double fahr(double);    // creates an external definition
double convert(int is_fahr, double temp) {
  /* A translator may perform inline substitutions */
  return is_fahr ? cels(temp) : fahr(temp);
}

ここでは、fahrextern 付きで再宣言されています。 で、外部から参照できる定義が生成される、と書かれてます。

つまり、 inline は、後から extern を付けて再宣言することで extern inline にできます。

例だとひとかたまりになっていますが、実際には次のようなパターンが想定されているのでしょう。

lib.h

inline int DOUBLE(int x){ return x * 2; }

lib.c

#include "lib.h"

extern inline int DOUBLE(int x); /* ここに外部から参照可能な実体を作る */

use.c

#include "lib.h"

int main(){
  int n = DOUBLE(2); /* 展開されるか、lib.cのシンボルを呼び出すかどちらか */
  ...
}

こうすることで #if などによる切り分けをせずに、inline関数の実体の定義を置くファイルを決める事ができます。 トリッキーな……。

C++のinlineは、常に実体が生成され、オブジェクトファイルの中でsharedセクション [1] として格納されています。 同じ内容を持つシンボルがオブジェクトファイルごとに作られますが、リンカがそれをひとつにまとめます。 なんて便利なのでしょう。 (暗黙にインスタンス化されるtemplateのあるC++ではこの機能がないと、あっちで使った vector<int> とこっちで使った vector<int> が別々のコードになって実行ファイルを凄く肥大化させる、ということになってしまいますので、大変嬉しい機能です。)

ですので、細かい使い分けの手間はありません。

[1] gccの場合。他のコンパイラでも相当する機能が使われるはず。

C99以前に独自にinlineを実装していたgccでは、なんと inlineextern inline の意味がC99と逆です。 extern inline に抱いた違和感からすると、この方が素直な実装とは言えます、しかし、実際問題、逆になってしまっているのです。

そのため、コンパイルオプションで -std=c99 或いは -std=gnu99 (またはc11等それ以降の版) を指定した場合は、もしくはgcc-5系以降のデフォルトではC99での意味。 -std=c90 或いは -std=gnu90 (またはc89等それ以前の版) を指定した場合、もしくはgcc-4.9系以前のデフォルトでは、逆転したgccでの意味となります。 頭痛いです。

__attribute__((__gnu_inline__)) を付けると、常時gccでの意味になります。 常時C99の意味にさせる方法はありません。

gccの意味の時は、 __GNUC_GNU_INLINE__ が、C99の意味の時は __GNUC_STDC_INLINE__ が定義されていますので、 #if で切り分けるしかないです。 コノヤロウ。

Thanks to @tnozaki. [2]

[2] 追記: tnozaki先生がツイートをまとめてくださいました。http://togetter.com/li/973289
⚠️ **GitHub.com Fallback** ⚠️