T240730 inline namespace
T240725 说到,Unnamed namespaces 有一个唯一的名称会通过 using-directives 自动导入,倘若可以手动指定这个名称,就是 inline namespaces 了。此时,自动导入的名称就是 inline namespace 的名称,比如 std::literals
和 std::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 版本。