インライン名前空間 101

ほぼ 3 年前 — うわー、時が経つのは早いものです — 私は名前空間エイリアスについてブログを書き、C++ で最も過小評価されている機能の 1 つと呼びました (これはおそらくクリックベイトのようなものでした)。

他の名前空間機能について話しましょう。過小評価されているわけではありませんが、あまり知られていません:inline 名前空間.それらは、スコープを導入する場合を除いて、実際にはスコープを導入しない名前空間です.

では、それらで何ができるでしょうか?

inline とは 名前空間?

C++11 導入 inline それらは、実際には名前空間ではない名前空間です。それらの内部で宣言されているものはすべて、親の名前空間の一部でもあります。

namespace foo // normal namespace
{
    void foo_func(); // function inside normal namespace
}

inline namespace bar // inline namespace
{
    void bar_func(); // function inside inline namespace
}

foo::foo_func(); // okay
bar::bar_func(); // also okay

foo_func(); // error, no such function
bar_func(); // okay, inline namespace!

これは…無意味に思えますか?

ただし、この機能には 2 つの使用例があります。

API バージョニング

いくつかのユーティリティ クラス foo を含むライブラリを作成したとします。 :

namespace my_library
{
    class foo
    {
        …
    };
}

しかし、あなたは foo に満足していません であるため、将来のバージョンでは大幅に改善されています。残念ながら、新しい foo 完全な下位互換性はありません:一部のユーザーは古いバージョンを使用する必要があります.

したがって、移行を容易にするために、引き続き両方を提供します。

namespace my_library
{
    namespace v1
    {
        // old foo
        class foo { … };
    }
    inline namespace v2
    {
        // new, improved foo
        class foo { … };
    }

    // note: no `foo` in `my_library` directly
}

ほとんどのユーザーは my_library::foo を使い続けています 静かに v2 を取得します version.v2 を使用できないユーザー my_library::v1::foo に切り替えるだけです 代わりに、これははるかに簡単に移行できます。

// on update it will get the shiny new v2 version
my_library::foo f;

// but maybe I don't want it, just change the namespace
my_library::v1::foo f;

しかし、なぜ inline が必要なのですか? そのための名前空間ですか?これだけではできませんか?

namespace my_library
{
    namespace v1
    {
        // old foo
        class foo { … };
    }
    namespace v2
    {
        // new, improved foo
        class foo { … };
    }

    using namespace v2;
}

その先 my_library::foo 同様に機能し、v2 に解決されます .

それは本当ですが、v2::foo まだ my_library の一部ではありません .これは ADL に影響を与えます (my_library 内を検索しません)。 )、テンプレートの特殊化など

ガイドライン :API の重大な変更を行う場合は、ネストされた inline を追加することを検討してください。 v2 古いものは入れ子になった v1 にあります。 古い API を保持する必要があるユーザーは、必要に応じて手動でオプトインするだけです。

ABI バージョニング

もう 1 つの使用例は、ABI のバージョニングです。ABI が何であるかを知らない場合は、幸運だと考えてください!

むかしむかし、人々は C ライブラリを作成し、それらを世界に出荷しました。ユーザーは、独自のプログラムを作成し、それらのライブラリにリンクして使用することができました。ライブラリの更新が利用可能で、ライブラリの API が変更されていない場合、プロジェクトを再コンパイルする必要はありません。プロジェクトを新しいバージョンに再リンクするだけです (動的にリンクされたライブラリの場合は何もしません):リンカーは、ライブラリのすべての呼び出しを新しい定義に解決します。

その後、C++ が登場し、すべてが変わりました。

C 関数をコンパイルする方法 (ABI) は OS 向けにほぼ標準化されていますが、C++ の場合はそうではありません。同じコンパイラとフラグ。

さらに、C での API の変更は、ABI の変更とほぼ 1 対 1 で対応していました。たとえば、関数にパラメーターを追加したり、struct にデータ メンバーを追加したりするようなものです。 C++ ではそうではありません:プログラムのコンパイル方法を変更する多くの API 互換の変更を行うことができます.まったく変更されていません!

これにより、ABI が変更されないように注意する必要がある、やや不安定な環境が作成されました。ABI を変更すると、呼び出し元のコードと呼び出されたコードが、メモリ内でのデータの配置方法に同意しない可能性があります。本当に奇妙なバグ!

ヘッダーと実装を含むライブラリを検討してください:

// library.h
namespace my_library
{
    class foo
    {
        int i = 42;

    public:
        void do_sth() const;
    };
}

// library.cpp
#include <iostream>

#include "library.h"

void my_library::foo::do_sth() const
{
    std::cout << i << '\n';
}

実行可能ファイルから呼び出すと、 42 が出力されます 、期待どおり:

// application.cpp
#include "library.h"

int main()
{
    my_library::foo f;
    f.do_sth();
}

ただし、ライブラリが次のように変更された場合に何が起こるかを考えてみてください:

// library.h
namespace my_library
{
    class foo
    {
        float f = 3.14; // new!
        int i = 42;

    public:
        void do_sth() const;
    };
}

// library.cpp
#include <iostream>

#include "library.h"

void my_library::foo::do_sth() const
{
    std::cout << i << '\n';
}

ライブラリを再コンパイルして再リンクしますが、しない アプリケーションを再コンパイルすると、1059720704 のような結果が得られます (UBです)!sizeof(foo) アプリケーションはまだ sizeof(int) です 、そして float については知りません member.But do_sth()float があります メンバーであるため、アプリケーションによって予約されたスペースの後、初期化されていないメモリにアクセスします。

ライフハック: 新しい依存関係のバージョンを取得するたびに、再コンパイルするだけです。これにより、作業が大幅に改善されます。

これは inline の場所です 名前空間が役立ちます。 inline の間 名前空間は C++ 側では完全に透過的ですが、アセンブリ レベルでは透過的ではありません。関数のマングルされた名前 — オーバーロードを可能にするために使用される翻訳版 — は そう します。 インライン名前空間を含みます。

foo を入れます inline に 名前空間:

// library.h
namespace my_library
{
    inline namespace abi_v1
    {
        class foo
        {
            int i = 42;

        public:
            void do_sth() const;
        };
    }
}

アプリケーション プログラムは 書き込み my_libray::foo しかし実際には 使用 my_library::abi_v1::foo .そして同様に、呼び出しは my_library::abi_v1::foo::do_sth() に行きます .

float を追加すると 、 abi_v2 に切り替えます my_library::abi_v1::foo::do_sth() がないため、再リンク時にリンカ エラーが発生します。 もう!再コンパイルする必要があるので、abi_v2 を呼び出します

そうすれば、謎の UB として具体化する代わりに、ABI の不一致が検出されます。

ガイドライン: ライブラリの作成者として、inline を追加することを検討してください ABI の重大な変更のたびに (または常に) 更新される ABI バージョンの名前空間。このように、ユーザーは新しいバージョンにリンクするために再コンパイルする必要があります。

ABI のバージョンは、API のバージョンに関連付ける必要はまったくないことに注意してください。ABI の重大な変更を行うたびに、またはユーザーに再コンパイルを求めるたびに、数値を変更するだけでかまいません。

特定の最適化を実装するために ABI の重大な変更が必要になることがよくあるため、ほとんどのライブラリは ABI の安定性を提供すべきではありません。ライブラリの実装者にとっては非常に困難になるだけです。そのため、ABI のバージョンは 多く .

また、ヘッダーのみのライブラリの場合は、まったく気にする必要がないことに注意してください。ユーザーは再リンクできません。

結論

inline 名前空間は便利なツールです。

API の下位互換性が気になる場合は、古いバージョンの API を新しいバージョンと並行して提供でき、通常のユーザーには完全に透過的です。

inline を変更すると すべての ABI 破壊的変更 (または、ABI の互換性が必要ない場合はリリース) で名前空間を変更することで、ユーザーが実際にプログラムを再コンパイルせずにライブラリに再リンクするだけで、不可解なバグを防ぐことができます。

最後に、ネストされた名前空間が気に入らなくても心配はいりません。C++20 では namespace my_library::inline v1 { と書くことができます。 これは、C++17 のネストされた名前空間宣言の優れた改善です。