[C++]inline名前空間の使途

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_literalssvリテラルだけが定義されている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名前空間APIC++コード上)からは透過的ですが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という名前でswapCPOを参照できますし、#1と#2のswapは別の名前空間にいるために名前は衝突していません。
このため、標準ライブラリにあるCPOはほとんどのものがinline名前空間に包まれています。

ここでは説明のために変な名前空間と適当なクラスを用意しましたが、mystdstdmystd::Sstd::vectorとかに読み替えるとつかみやすいかもしれません。

参考文献

この記事のMarkdownソース