T240725 说到,Unnamed namespaces 有一个唯一的名称会通过 using-directives 自动导入,倘若可以手动指定这个名称,就是 inline namespaces 了。此时,自动导入的名称就是 inline namespace 的名称,比如 std::literalsstd::liternals::chrono_literals实现

#if __cplusplus >= 202002L
  inline namespace literals
  {
  inline namespace chrono_literals
  {
    /// @addtogroup chrono
    /// @{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wliteral-suffix"
    /// Literal suffix for creating chrono::day objects.
    /// @since C++20
    constexpr chrono::day
    operator""d(unsigned long long __d) noexcept
    { return chrono::day{static_cast<unsigned>(__d)}; }

    /// Literal suffix for creating chrono::year objects.
    /// @since C++20
    constexpr chrono::year
    operator""y(unsigned long long __y) noexcept
    { return chrono::year{static_cast<int>(__y)}; }
#pragma GCC diagnostic pop
    /// @}
  } // inline namespace chrono_literals
  } // inline namespace literals
#endif // C++20

正因如此,std/std::literals/std::literals::chrono_literals 任选其一,都可以直接使用时间相关的 Literals。例子:

#include <chrono>
#include <iostream>

int main()
{
    // using namespace std;
    // using namespace std::literals;
    using namespace std::literals::chrono_literals;
    auto day = 24h;
    auto halfhour = 0.5h;
    std::cout << "one day is " << day.count() << " hours (" << day << ")\n"
              << "half an hour is " << halfhour.count() << " hours ("
              << halfhour << ")\n";
}

因此,inline namespaces 最基本的作用就是影响名称查找规则,既可以使用命名空间显式地导入组件,也可以使用隐式的导入行为。但需记住,这并不会像 Unnamed namespaces 那样改变原有的链接方式。

基于这一特性,可控制库 API 版本的向后兼容性。比方说现在库中有一个 S::foo 函数:

namespace mylib {

    namespace v0 {

        struct S {
            void foo() {
                std::cout << "vo::foo()\n";
            }
        };

    } // namespace v0

} // namespace mylib

int main()
{
    using namespace mylib::v0;
    S s;
    s.foo();
}

在下一版本中,需要修改 S 类的结构,这里假设把 foo() 的名称变成 bar()。只要是公开发布、存在用户群体的库,直接修改自是不成的,那样别人只要稍微升级一下库的版本,旧代码就一片崭红。于是,可以新增加一个 v1 版本的实现:

namespace mylib {

    namespace v0 {

        struct S {
            void foo() {
                std::cout << "vo::foo()\n";
            }
        };

    } // namespace v0

    namespace v1 {

        struct S {
            void bar() {
                std::cout << "v1::bar()\n";
            }
        };

    } // namespace v1

} // namespace mylib

int main()
{
    using namespace mylib::v1;
    S s;
    s.bar();
}

若是想用旧的,依旧使用 using namespace mylib::v0 即可保证代码兼容。但是,哪个库每次使用还需要手动写明 v0/v1... 啊,用户并不一定清楚库的版本,通常来说,他们只想使用最新的版本。这便是 inline namespaces 的用武之地,改变代码为:

// ......

    inline namespace v1 {

        struct S {
            void bar() {
                std::cout << "v1::bar()\n";
            }
        };

    } // namespace v1

} // namespace mylib

int main()
{
    using namespace mylib;
    S s;
    s.bar();
}

如此一来,只需使用 using namespace mylib,便能静默切到最新的实现版本。若想兼容旧代码,则手动切换为指定的版本即可。

你可能会觉得以下这种方式也能达到同样效果,用不用 inline namespaces 不是必须。

// ......

    namespace v1 {

        template<typename T>
        struct S {
            void bar() {
                std::cout << "v1::bar()\n";
            }
        };

    } // namespace v1

    using namespace v1;

} // namespace mylib

int main()
{
    using namespace mylib;
    S<int> s;
    s.bar();
}

但是,当你尝试在命名空间之外特化 S 时:

// ......

    namespace v1 {

        template<typename>
        struct S {
            void bar() {
                std::cout << "v1::bar()\n";
            }
        };

    } // namespace v1

    using namespace v1;

} // namespace mylib

namespace mylib {

    // specialization outside its namespace
    template<>
    struct S<void> {
        void bar() {
            std::cout << "void v1::bar()\n";
        }
    }

} // namespace mylib

int main()
{
    using namespace mylib;
    S<int> s;
    s.bar();
}

就会遇到编译错误,而如果使用 inline namespaces 就不会存在这个错误。

inline namespaces 的另一个作用是控制 ABI 版本,当 ABI 改变时,调用代码和被调用代码的数据内存布局将会变得不一致,这往往会产生 UB。例如:

// lib.h
namespace mylib {
    struct S {
        int x = 42;

        void foo() const;
    };
} // namespace mylib

// lib.cpp
#include "lib.h"
#include <iostream>

void mylib::S::foo() const {
    std::cout << x << '\n';
}

// main.cpp
#include "lib.h"

int main() {
    mylib::S s;
    s.foo();
}

lib.cpp 编译成共享库,再用 main.cpp 链接该库。

$> g++ -fPIC -shared -o libs.so lib.cpp
$> g++ -o main main.cpp -L. -ls -Wl,-rpath,.
$> ./main
42

这里正常情况,接着改变一下类的结构,API 保持不变:

// lib.h
namespace mylib {
    struct S {
        char c = '+';
        int x = 42;

        void foo() const;
    };
} // namespace mylib

// lib.cpp
#include "lib.h"
#include <iostream>

void mylib::S::foo() const {
    std::cout << c << '\n';
}

重新编译生成链接库,不必重新编译 main.cpp,直接链接输出:

$> g++ -fPIC -shared -o libs.so lib.cpp
$> ./main
*

可见,ABI 已经发生了改变,结果变得不可预料,此时最简单的解决办法就是重新编译 main.cpp。但 inline namespaces 可以更好的定位此问题,将新旧代码同样使用 v0/v1 区分版本,ABI 改变时,函数的 Mangled name 将不一致,从而产生链接错误。

具体来讲,将代码变成这样:

// lib.h
namespace mylib {

    namespace v0 {

        struct S {
            int x = 42;

            void foo() const;
        };

    } // namespace v0

   inline namespace v1 {

       struct S {
           char c = '+';
           int x = 42;

           void foo() const;
       };

   } // namespace v1

} // namespace mylib

// lib.cpp
#include "lib.h"
#include <iostream>

void mylib::S::foo() const {
    std::cout << c << '\n';
}

再企图将可执行程序链接到新的共享库时,将产生编译期错误:

$> g++ -fPIC -shared -o libs.so lib.cpp
$> ./main
./main: symbol lookup error: ./main: undefined symbol: _ZNK5mylib2v01S3fooEv

这样就能够清晰地定位 ABI 问题,要么重新编译程序,要么链接匹配的 ABI 版本。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.