#概要
inline
指定子は、かつてコンパイラのインライン化を促進するために使われた。
コンパイラが賢くなった現代では、インライン化を促進する意味でのinline
指定子は、実際上何の影響も与えない。
ところが、inline
指定子にはコンパイラへのヒントだけでなく、特に複数の翻訳単位が有る時、プログラムに影響を与える事がある。
#例1
// test1.cpp
#include <iostream>
inline int f() { return 1; }
void g() { std::cout << f() << std::endl; }
int main()
{
g();
}
// test2.cpp
inline int f() { return 2; }
void h() { f(); }
この二つの翻訳単位から成るプログラムをコンパイルすると、面白い結果になる。
すなわち、リンク順序に結果が依存し、コンパイラはエラーも警告も出さない。
clang++ test1.cpp test2.cpp -o a.out
=> 1
clang++ test2.cpp test1.cpp -o a.out
=> 2
C++に慣れた人なら、inline int f()
がOne Definition Rule違反だと見抜くだろう。
これは、割としられている例で、プログラム全体で、外部リンケージを持つinline functionが多重定義されていた場合、未定義動作を引き起こす。
この未定義動作をコンパイラは検知しなくて良いと規格で定められている。
解決法は簡単で、inline functionが外部リンケージを持たなければ良い。
内部リンケージを持たせるには、static
指示子を用いたり、無名名前空間内でinilne functionを定義すれば良い。
あるいは、inline指定子を用いなければ良い。
さらに、同様な理由で、コンパイラが検知しない未定義動作を起こす物がいくつかある。
例えば、外部リンケージを持つクラスのメンバ関数のODR違反である。
また、constexpr functionも暗黙にinline specifiedである。
#例2
前の例は、異なって定義された外部リンケージinline functionが問題であった。
同じ定義を持っていてもODR違反となる場合がある。
// test1.cpp
#include <iostream>
const int x = 1;
inline int f() { return x; }
void g() { std::cout << f() << std::endl; }
int main()
{
g();
}
// test2.cpp
const int x = 2;
inline int f() { return x; }
void h() { f(); }
この例では、inline int f()
の定義は、はどの翻訳単位でも同じトークン列である。
また、const int x
は内部リンケージを持つ。
にも関わらず、このコードは先ほどと同様なリンク順序の非対称性を持つ。
これは、同じトークン列によって多重定義された関数の中で言及される、内部リンケージを持つconst変数は、全て同じ値で初期化されていなければならないという条件を破っている。
これも、規格によってODR違反とされる(例えばC++11標準規格なら、§3.2/5)が、コンパイラはやはり警告してくれない。
#まとめ
外部リンケージを持つinline関数はしばしば未定義動作を起こす。
外部ライブラリによって、グローバル名前空間が汚染されていた場合、もはや安全にグローバル名前空間を使う事はできない。
自分が作っているのが(翻訳単位を分ける事を想定している)ライブラリならば、何を外部リンケージとするか慎重に選んだ方が良い。
また、以上に挙げた以外にも、ODRに関連して未定義動作を引き起こす典型例がある。(参考参照)
勉強になった。
#参考
The One Definition Rule in C++11 and C++14: Constant Expressions and Inline Functions
stackoverflow:Is it an undefined behavior to have different definitions of an inline function?
本の虫:C++11参考書
本の虫:テンプレートの実体化の実装方法とODR違反について