C++17では変数にinline
キーワードを指定できるようになった。
inline int variable ;
このような変数をinline
変数と呼ぶ。その意味はinline
関数と同じだ。
今は昔、本書執筆から30年以上は昔に、inline
キーワードがC++に追加された。
inline
の現在の意味は誤解されている。
inline
関数の意味は、「関数を強制的にインライン展開させるための機能」ではない。
大事なことなのでもう一度書くが、inline
関数の意味は、「関数を強制的にインライン展開させるための機能」ではない。
確かに、かつてinline
関数の意味は、関数を強制的にインライン展開させるための機能だった。
関数のインライン展開とは、たとえば以下のようなコードがあったとき、
int min( int a, int b )
{ return a < b ? a : b ; }
int main()
{
int a, b ;
std::cin >> a >> b ;
// aとbのうち小さい方を選ぶ
int value = min( a, b ) ;
}
この関数min
は十分に小さく、関数呼び出しのコストは無視できないオーバーヘッドになるため、以下のような最適化が考えられる。
int main()
{
int a, b ;
std::cin >> a >> b ;
int value = a < b ? a : b ;
}
このように関数の中身を展開することを、関数のインライン展開という。
人間が関数のインライン展開を手で行うのは面倒だ。それにコードが読みにくい。"min(a,b)
"と"a<b?a:b
"のどちらが読みやすいだろうか。
幸い、C++コンパイラーはインライン展開を自動的に行えるので人間が苦労する必要はない。
インライン展開は万能の最適化ではない。インライン展開をすると逆に遅くなる場合もある。
たとえば、ある関数をコンパイルした結果のコードサイズが1Kバイトあったとして、その関数を呼んでいる箇所がプログラム中に1000件ある場合、プログラム全体のサイズは1Mバイト増える。コードサイズが増えるということは、CPUのキャッシュを圧迫する。
たとえば、ある関数の実行時間が関数呼び出しの実行時間に比べて桁違いに長いとき、関数呼び出しのコストを削減するのは意味がない。
したがって関数のインライン展開という最適化を適用すべきかどうかを決定するには、関数のコードサイズが十分に小さいとき、関数の実行時間が十分に短いとき、タイトなループの中など、さまざまな条件を考慮しなければならない。
昔のコンパイラー技術が未熟だった時代のC++コンパイラーは関数をインライン展開するべきかどうかの判断ができなかった。そのためinline
キーワードが追加された。インライン展開してほしい関数をinline
関数にすることで、コンパイラーはその関数がインライン展開するべき関数だと認識する。
現代では、コンパイラー技術の発展によりC++コンパイラーは十分に賢くなったので、関数をインライン展開させる目的でinline
キーワードを使う必要はない。実際、現代のC++コンパイラーではinline
キーワードはインライン展開を強制しない。関数をインライン展開すべきかどうかはコンパイラーが判断できる。
inline
キーワードにはインライン展開以外に、もう1つの意味がある。ODR(One Definition Rule、定義は1つの原則)の回避だ。
C++では、定義はプログラム中に1つしか書くことができない。
void f() ; // OK、宣言
void f() ; // OK、再宣言
void f() { } // OK、定義
void f() { } // エラー、再定義
通常は、関数を使う場合には宣言だけを書いて使う。定義はどこか1つの翻訳単位に書いておけばよい。
// f.h
void f() ;
// f.cpp
void f() { }
// main.cpp
#include "f.h"
int main()
{
f() ;
}
しかし、関数のインライン展開をするには、コンパイラーの実装上の都合で、関数の定義が同じ翻訳単位になければならない。
inline void f() ;
int main()
{
// エラー、定義がない
f() ;
}
しかし、翻訳単位ごとに定義すると、定義が重複してODRに違反する。
C++ではこの問題を解決するために、inline
関数は定義が同一であれば、複数の翻訳単位で定義されてもよいことにしている。つまりODRに違反しない。
// a.cpp
inline void f() { }
void a()
{
f() ;
}
// b.cpp
// OK、inline関数
inline void f() { }
void b()
{
f() ;
}
これは例のために同一のinline
関数を直接記述しているが、inline
関数は定義の同一性を保証させるため、通常はヘッダーファイルに書いて#include
して使う。
inline
変数は、ODRに違反せず変数の定義の重複を認める。同じ名前のinline
変数は同じ変数を指す。
// a.cpp
inline int data ;
void a() { ++data ; }
// b.cpp
inline int data ;
void b() { ++data ; }
// main.cpp
inline int data ;
int main()
{
a() ;
b() ;
data ; // 2
}
この例で関数a
, b
の中の変数data
は同じ変数を指している。変数data
はstatic
ストレージ上に構築された変数なのでプログラムの開始時にゼロで初期化される。2回インクリメントされるので値は2となる。
これにより、クラスのstatic
データメンバーの定義を書かなくてすむようになる。
C++17以前のC++では、以下のように書かなければならなかったが、
// S.h
struct S
{
static int data ;
} ;
// S.cpp
int S::data ;
C++17では、以下のように書けばよい。
// S.h
struct S
{
inline static int data ;
} ;
S.cpp
に変数S::data
の定義を書く必要はない。
機能テストマクロは__cpp_inline_variables
, 値は201606。