inline
名前空間はC++11から追加された機能で、その中にあるものは透過的に(名前空間がないかのように)アクセスすることができます。一見使いどころがなく見られがちですが、うまく使えばとても便利に活用することができます。
1. using namespace
の範囲を限定する
これは標準ライブラリではユーザー定義リテラルの定義と利用でよく利用されます。
例えば、std::string_view
を簡易利用するためのsv
リテラルは次のように宣言されています。
namespace std { // string_view本体 template <class CharT, class Traits = char_traits<CharT>> class basic_string_view; inline namespace literals { inline namespace string_view_literals { // svリテラル constexpr std::string_view operator""sv(const char* str, std::size_t len) noexcept; } } }
これによって、using namespace std;
という広すぎる範囲をusing
することなく、次のいずれかによってこのsv
リテラルを使用することができます。
#include <string_view> int main() { { // 1、標準ライブラリの定義する全リテラルだけが使用可能になる using namespace std::literals; auto str = "literals"sv; } { // 2、string_viewのsvリテラルだけが使用可能になる using namespace std::string_view_literals; auto str = "string_view_literals"sv; } { // 2、std名前空間の神羅万象が利用可能になる using namespace std; auto str = "std"sv; } }
std::literals
名前空間は標準ライブラリの定義するすべてのユーザー定義リテラル(s, h, m, s, i
などなど)が定義されているinline
名前空間であり、それをusing namespace
するとそれらのリテラルの全てがそのスコープで見えるようになります。
std::string_view_literals
はsv
リテラルだけが定義されているinline
名前空間であり、using namespace
してもsv
リテラル以外のものは見えません。
そして、これらのユーザー定義リテラルはstd
名前空間直下からも参照できます。
このようにライブラリの提供するものをinline
名前空間である程度グループ化しておくことで、使う側は適宜必要な範囲だけをusing namespace
することができるようになります。
2. APIのバージョニング
これは多分最もポピュラーなinline
名前空間の使い方でしょうか。
例えば、既に利用している関数があり、その関数のAPI(インターフェース)は変えないけれども内部実装を変更したい時を考えます。
namespace mylib { int f(int a) { return a; } }
この処理を、2倍してから返すようにしたいとします。
namespace mylib { int f(int a) { return 2 * a; // 2倍して返すようにしたい } }
しかし、この関数は既に至るところで使われており、変更するならしっかりテストしてからにしたいし、使われているとこを書き換えて回るのは嫌です。そんな時、変更前のバージョンをinline
名前空間で、変更後のバージョンを名前空間で囲っておきます。
namespace mylib::inline v1 { // 現在の処理 int f(int a) { return a; } } namespace mylib::v2 { // 変更後の処理 int f(int a) { return 2 * a; // 2倍して返すようにしたい } }
そして、新しい関数mylib::v2::f()
の実装とテストが完了してすべてのf()
を新バージョンへ切り替えることができるようになったら、それぞれの名前空間のinline
指定を逆にします。
// 古いバージョンを非inlineに namespace mylib::v1 { // 現在の処理 int f(int a) { return a; } } // 新しいバージョンをinlineに namespace mylib::inline v2 { // 変更後の処理 int f(int a) { return 2 * a; // 2倍して返すようにしたい } }
inline
名前空間は透過的です。そのため、これだけで、f()
を使用しているところを一切書き換えることなくその処理内容をアップデートできます。もし古いバージョンを使いたい場合はmylib::v1::f()
のように名前空間を明示的に指定してやればよく、古いバージョンを使用していることも分かりやすくなります。
3. ABIのバージョニング
inline
名前空間はAPIでは省略可能ですが、ABI(マングル名)では通常の名前空間と同じ扱いをされ、常にマングル名に表示されていますし、参照するときも省略できません。inline
名前空間の効果は純粋にC++の意味論上だけのものです。
この性質によって、APIは変わらないけれどABIを破壊するような変更がなされたときにその影響を軽減することができます。
/// header.h namespace mylib { class S { int m = 10; public: int get_m() const; }; }
/// source.cpp #include "header.h" namespace mylib { int S::get_m() const { return this->m; } }
/// main.cpp #include <iostream> #include "header.h" int main() { mylib::S s{}; std::cout << s.get_m(); }
例えばこんなクラスとそのメンバ関数がありヘッダと実装が分かれているとき、これを利用するmain.cpp
が同じコンパイラを使ってsource.cpp
をコンパイルしている間は何も問題はありません。
しかし、source.cpp
を静的ライブラリや動的ライブラリの形であらかじめコンパイルしてから利用しているときに、このmylib::S
にABIを破壊する変更がなされてしまったとします。
/// header.h namespace mylib { class S { float f = 1.0; // メンバを追加した int m = 10; public: int get_m() const; }; }
このような変更はAPIに何も影響を及ぼしませんが、クラスのレイアウトが変更されているのでABIから見ると重大な変更です。ABIを破壊しています。
このとき、source.cpp
をコンパイルした静的or動的ライブラリを再コンパイルせずに使い続けて(リンクして)いたとしてもコンパイラは何も言わないでしょう。この場合の変更によってはマングル名は変化しておらず、コンパイルもリンクもつつがなく完了します。
/// main.cpp #include <iostream> // これは最新のものを参照しているとする #include "header.h" // source.cppは10年前にコンパイルしたものをリンクして使い続けているとする int main() { mylib::S s{}; std::cout << s.get_m(); // 未定義動作! }
古いS::get_m()
関数の定義はsource.cpp
をコンパイルした外部ライブラリにあり、新しいmylib::S
のレイアウトを知りません。したがって、レイアウト変更後のクラスのどこかの領域をint
型のメンバS::m
として読みだした値を返してくれるでしょう(たぶん実行時エラーも起きないのではないかと思われます)。これは紛う事なき未定義動作です・・・
これは稀によくあるビルド済みバイナリをリンクして利用する時の問題で、C++でヘッダオンリーライブラリが好まれる傾向にある事の一つの理由でもあります。
こんな時、inline
名前空間を利用することでこの問題の軽減を図れます。
リンクエラーにする
解決策の一つ目は、変更後のコードをそれを表すinline
名前空間で囲ってしまう事です。
/// header.h // inline名前空間を追加する namespace mylib::inline v2 { class S { float f = 1.0; // メンバを追加した int m = 10; public: int get_m() const; }; }
/// source.cpp #include "header.h" // inline名前空間を追加する namespace mylib::inline v2 { int S::get_m() const { return this->m; } }
inline
名前空間はAPI(C++コード上)からは透過的ですがABI(マングル名)には表示されます。従って、この変更後のmylib::S
およびそのメンバ関数を利用するコードに変更は必要ありませんが、そのマングル名は変更前のものと異なっています。
結果、再コンパイルしないで用いている変更前ソースによるビルド済みバイナリからはそのようなシンボルが見つからずリンクエラーによってコンパイル時に気づくことができます。
/// main.cpp #include <iostream> // これは最新のものを参照しているとする #include "header.h" // source.cppは10年前にコンパイルしたものをリンクして使い続けているとする int main() { mylib::S s{}; std::cout << s.get_m(); // リンクエラー、シンボルが見つからない }
ABI互換性を確保する
もう一つの方法は初めからinline
名前空間を利用していた場合にのみ利用可能となります。例えば先程のサンプルは初めから次のようにinline
名前空間に囲まれていたとします。
/// header.h // inline名前空間に包まれている namespace mylib::inline v1 { class S { int m = 10; public: int get_m() const; }; }
/// source.cpp #include "header.h" // inline名前空間に包まれている namespace mylib::inline v1 { int S::get_m() const { return this->m; } }
このコードに対して先程の変更がなされたとしましょう。その際、古いバージョンのinline
名前空間を非inline
にし、新しいバージョンのinline
名前空間名を変更しておきます。
/// header.h // 古いバージョン、inline名前空間ではなくする namespace mylib::v1 { class S { int m = 10; public: int get_m() const; }; } // 最新版 namespace mylib::inline v2 { class S { float f = 1.0; // メンバを追加した int m = 10; public: int get_m() const; }; }
/// source.cpp #include "header.h" // 古いバージョン、inline名前空間ではなくする namespace mylib::v1 { int S::get_m() const { return this->m; } } // 最新版 namespace mylib::inline v2 { int S::get_m() const { return this->m; } }
こうしておいた状態で、さっきまでのように使用しようとすれば正しくリンクエラーになります。
今回のようにした場合は、逆に変更前のバージョンのものを参照し続けているようなプログラムに最新のsource.cpp
をビルドした(変更が反映された)バイナリをリンクした時でも、リンクエラーも未定義動作も起こさずに使用することができます。
/// main.cpp // 10年前のコードを参照ヘッダも含めて変更せずに使い続けているとする #include <iostream> // 変更前のものを参照している #include "header.h" // source.cppはさっきコンパイルした最新のものをリンクしているとする int main() { // mylib::v1::Sを参照している mylib::S s{}; // mylib::v1::S::get_m()を参照している std::cout << s.get_m(); // 10 }
inline
名前空間はマングル名レベルでは名前空間と区別なく扱われます。すなわち、ABIからは名前空間名がinline
であるかどうかは分かりません。したがって、このように変更を追記し古いバージョンを維持しておけば、古いバージョンを利用しているプログラムに対しても古いバージョンを提供し続けることができます。これによって、ABI互換性を維持し、ABIを保護することができます。
GCCやclangの最近の標準ライブラリの実装では、ABI保護のためにinline
名前空間が多用されています。
4. 名前の衝突を回避する
これは特にCPO(Customization Point Object)の定義で利用されています。
CPOはその呼び出しに当たって自身と同名の非メンバ関数をADLで探索するようになっていることがよくあります。これはあるクラスに対してHidden friendsと呼ばれるfriend
関数を探し出すものです。
一方で、標準ライブラリにあるCPOは一部のものを除いてstd
名前空間のすぐ下に定義されることになります。
すると、標準ライブラリにあるクラスに対するHidden friends関数とCPOとで名前が衝突してしまいます。
namespace mystd { namespace cpo_impl { // swap CPOの実装クラス struct swap_cpo { template<typename T, typename U> void operator()(T&, U&) const; }; } // swap CPO #1 inline constexpr cpo_impl::swap_cpo swap{}; struct S { // Hidden friendsなswap関数 #2 friend void swap(S& lhs, S& rhs); }; }
この例では#1と#2の異なる宣言が同じ名前空間にあるためにコンパイルエラーになっています。
このように、CPOと同じ名前空間にあるものがそのCPOにアダプトしようとすると名前衝突してしまうわけです。
この場合にCPOの定義をinline
名前空間で囲ってやることでこの問題を解決できます。しかも、呼び出す際に名前空間名が増えることもありません。
namespace mystd { namespace cpo_impl { // swap CPOの実装クラス struct swap_cpo { template<typename T, typename U> void operator()(T&, U&) const; }; } // CPO定義をinline名前空間で囲う inline namespace cpo { // swap CPO #1 inline constexpr cpo_impl::swap_cpo swap{}; } struct S { // Hidden friendsなswap関数 #2 friend void swap(S& lhs, S& rhs); }; }
こうしてもmystd::swap
という名前でswap
CPOを参照できますし、#1と#2のswap
は別の名前空間にいるために名前は衝突していません。
このため、標準ライブラリにあるCPOはほとんどのものがinline
名前空間に包まれています。
ここでは説明のために変な名前空間と適当なクラスを用意しましたが、mystd
をstd
、mystd::S
をstd::vector
とかに読み替えるとつかみやすいかもしれません。