C++にはinline
指定子があります。しかし、このinline
指定子は、その名前から誤解されがちです。
(誤解)inline
指定子を付けると、その関数はインライン展開される
これは誤解であり、正しくありません。正しくは、以下のような意味になります。
inline
指定子を付けた場合、複数回の定義が許される。また、複数の翻訳単位*1で定義されたとしても実体は一つになることが保証される*2。(C++14まで)inline
指定子を付けた場合、コンパイラはその関数をインライン展開するヒントとすることができる。
「inline指定子を付けた場合、コンパイラはその関数をインライン展開するヒントとすることができる。」という記述はC++17以降で無くなりました。
ただし、コンパイラの最適化は見える結果が変わらない範囲で自由に行えるため、inline指定子をヒントにすることは問題ありません。(2020/09/02 なくなっていないとの指摘を受けましたので削除)
ちなみに、テンプレート関数、constexpr
関数、クラス定義内で定義される関数、はいずれも暗黙にinline
指定されたものとみなされます。default
定義されたメンバ関数も暗黙にinline
指定されたものとみなされます。
ところで、inline
指定されている場合(これには上述の暗黙指定の場合を含みます)、複数回の定義が許されますが、その場合でも全ての定義は字面上同一かつ意味上同一でないといけません(one definition rule, ODR)。
外部リンケージを持つ(static
指定されておらず、無名名前空間に囲まれていない)関数がODRを守れていない場合、コンパイラは特に指摘してくれません(診断不要:no diagnostics required)。このようなことが発生すると、リンクの順番によってプログラムの実行結果が変わるなどの意味不明なバグにつながり、非常にデバッグが困難です。つまり、うっかり同名の関数や変数を書いてしまってもコンパイラが指摘してくれず、バグが埋め込まれるということになります。そのため、特にグローバル名前空間にinline
なものを置くことは忌避されるべきです。
うっかりを防ぐためには、static inline
とすべきです。ただし、実行効率まで考えると常に最善とは限りません(後述します)。
(誤解)inline
指定子を付けると、その関数はインライン展開されやすくなる
先ほどの誤解よりは少しましになりましたが、依然誤解です。「コンパイラはその関数をインライン展開するヒントとすることができる」をうがって読むと、「コンパイラはその関数をインライン展開 しない ことの根拠としても使える」ことがわかります。そんな例は実在するのかという感じですが、以下に実例を示します。
inc.h
#include <array> #include <cstdint> #define M1(i) s += (i)*(i), arr[s%SIZE] = i; #define M2(i) M1(i) M1(i+1) #define M4(i) M2(i) M2(i+2) #define M8(i) M4(i) M4(i+4) #define M16(i) M8(i) M8(i+8) #define M32(i) M16(i) M16(i+16) #define M64(i) M32(i) M32(i+32) #define M128(i) M64(i) M64(i+64) DECL_SPEC std::array<std::size_t, 128> LargeFunction(std::size_t s) { std::array<std::size_t, 128> arr {}; for( int t = 0; t < 128; ++t ) { M128(0); } return arr; }
source1.cpp
#include "inc.h" std::array<std::size_t, 128> callLargeFunction(std::size_t s) { return LargeFunction(s); }
source2.cpp
#include "inc.h" std::array<std::size_t, 128> anotherCallLargeFunction(std::size_t s) { return LargeFunction(s); }
main.cpp
#include <iostream> #include <array> #include <cstdint> std::array<std::size_t, 128> callLargeFunction(std::size_t s); std::array<std::size_t, 128> anotherCallLargeFunction(std::size_t s); int main() { std::cout << callLargeFunction(1)[3] << std::endl; std::cout << anotherCallLargeFunction(2)[4] << std::endl; }
以下のコンパイルオプションで分割コンパイルし、リンクした場合、バイナリのサイズは13120バイトとなります。コンパイルオプションとして-Os
の代わりに-O1
、-O2
、-O3
を使う場合、17176バイトとなります。-flto
オプションを付ける場合、四つの最適化オプション-O1
、-O2
、-O3
、-Os
のいずれでも12936バイトとなります。
g++-8 -std=c++17 -Os -DDECL_SPEC=inline source1.cpp -c -o source1_inline.o g++-8 -std=c++17 -Os -DDECL_SPEC=inline source2.cpp -c -o source2_inline.o g++-8 -std=c++17 -Os main.cpp -c -o main_inline.o g++-8 source1_inline.o source2_inline.o main_inline.o -o inline
一方、-DDECL_SPEC=inline
の代わりに-DDECL_SPEC=static
や-DDECL_SPEC='static inline'
を指定した場合、最適化オプションによらず17176バイトとなります。また、-flto
オプションを付けた場合、-O1
の場合のみ12896バイト、その他の三つの最適化オプション-O2
、-O3
、-Os
のいずれでも12936バイトとなります。
つまり、 static
やstatic inline
の代わりにinline
とすることでインライン展開を抑制することができる 場合がある(具体的には-Os
指定かつ-flto
指定なし)ということです。
なぜこのようなことが起こったのでしょうか?
-flto
オプションを付けた場合、他の翻訳単位で使われるか否かを見てからインライン展開をするかどうかを判断することができるようにコンパイルすることになります。よって、基本的にはインライン展開せずにコンパイルし、リンク時にインライン展開を行うか判断することになります。そのため約13KBの小さなバイナリとなります。
一方、-flto
オプションを付けない場合、他の翻訳単位で使われるか否かはわからないままインライン展開するべきかの判断を下す必要があります。インライン展開をすべきと判断した場合、二つの関数にインライン展開されるためバイナリサイズが増大し、約17KBのバイナリとなります。
ここで、static
指定子がついている場合(-DDECL_SPEC=static
と-DDECL_SPEC='static inline'
とした場合)を考えてみると、他の翻訳単位からその関数が呼ばれることはない*3ため、実体を作る必要はありません。そのため、その翻訳単位で一回しか呼ばれないのならインライン展開してしまうのが最善となります。
一方、単独でinline
としている場合、「複数の翻訳単位で定義されたとしても実体は一つになることが保証される」ことから、インライン展開しないで実体を作っておけば、他の翻訳単位で作った実体とリンク時にマージできる可能性があり、バイナリサイズを小さくする役に立てることができます。そのため、むしろインライン展開を抑制する働きを持つことがあります。
まとめ
inline
指定子はもはやコンパイラにインライン展開を指示するものではありません。また、インライン展開を促進する効果があるとも限らず、むしろインライン展開を抑制する働きを持つことさえあります。この効果はstatic inline
とした場合には働かないため、ODR違反に細心の注意を払いつつstatic inline
としないことが有効な局面もあり得るということを示しました。
補遺
inline
はむしろインライン展開を抑制する働きを持つことさえある、と書きましたが、これはあくまで特殊例です。現にコンパイラはinline
キーワードがついている関数についてインライン展開閾値を下げるということを行っているようです。
また、関数のマージはinline
を付けない限り発生しないわけではなく、コンパイラの最適化によって勝手に行われることがあるようです。しかも、関数ポインタをとっているのに勝手にマージされて同じアドレスになって困るなどの現象も発生することがあったらしいです(VC9で発生した例→RTTI を使わずに Boost.Any を実装する方法 - melpon日記 - HaskellもC++もまともに扱えないへたれのページ)。