重新认识C++的inline关键字
前言
一些教程和文章里对C++中的inline
关键字的说明是这样的:用来建议编译器对被修饰的函数进行内联展开优化。包括我自己也在初学C++的很长一段时间里也是这么认为的,这大概是从C语言转到C++带来的“遗留问题”。而实际上自C++98起,inline
关键字的作用就已经从“优先内联”变成了“允许多次定义”。因为内联替换在标准语义中是不可观察的,现代编译器几乎不会参考函数声明中的inline
修饰符来决定是否内联,编译器拥有对任何非标记为inline
的函数进行内联替换的自由,也拥有对任何标记为inline
的函数生成函数调用的自由,它自有决断,没有哪个符合标准的主流编译器能保证一定内联或者一定不内联(至少我没见过)。既然如此,那不如让inline
关键字在其他地方发光发热好了,于是自C++98起,inline
关键字对函数的作用变为“允许多次定义”,自C++11起,增加对命名空间的修饰,自C++17起,增加对变量的修饰。
inline修饰函数
学过C语言的读者都知道,非static
的函数定义不要写在头文件中(准确地说是被多个源文件包含的头文件),不然在链接的时候会发生multiple definition错误。而在C++98之后,声明为inline
的函数可以被多次定义,只要每个定义都在不同的翻译单元中即可。注意,如果具有外部链接的函数在不同的翻译单元中定义不同,那么程序非良构。以下示例代码是正确的:
// foo.hpp
#include <iostream>
inline void foo() {
std::cout << "foo()\n";
}
// bar.cpp
#include "foo.hpp"
void bar() {
foo();
}
// bar.hpp
void bar();
// main.cpp
#include "foo.hpp"
#include "bar.hpp"
int main() {
bar();
return 0;
}
inline修饰命名空间
C++11引入了内联命名空间(inline namespace),内联命名空间是在它的原初命名空间定义中使用了可选的关键词inline
的命名空间。对于一个内联命名空间,它内部所包含的成员的可见性就像声明在外围命名空间一样,就像inline
字面意思上的“展开”。但是与直接声明在外围命名空间中又有所不同,它既将内部成员暴露了出来,又具有命名空间最根本的作用:防止命名冲突。例如在内部内联命名空间内定义了函数void foo();
,在外部仍然可以定义void foo();
,甚至在另一个同级的内联命名空间中也能定义void foo();
。内联命名空间最主要的作用是用于库的版本控制与API演进。来看下面的例子:
namespace Lib {
inline namespace v2 {
void foo() {
std::cout << "v2 foo" << std::endl;
}
}
// 由于上面已经声明过了v2为inline,所以这里的v2也隐式的内联了
namespace v2 {
void bar() {
std::cout << "v2 bar" << std::endl;
}
}
namespace v1 {
void foo() {
std::cout << "v1 foo" << std::endl;
}
}
}
int main() {
// 默认调用最新版本v2的API
Lib::foo(); // 输出 "v2 foo"
Lib::bar(); // 输出 "v2 bar"
// 当用户需要继续使用旧版本v1的API时
Lib::v1::foo(); // 输出 "v1 foo"
return 0;
}
上面的是库版本从v1
迭代到v2
时,使用内联命名空间进行版本更新,并同时保留旧版本实现的例子。假如未来要升级到v3
版本,只需增加一个v3
命名空间,并将唯一一个inline
关键字移动到v3
命名空间的声明即可。
对于上面的例子,可能有的读者会看出来不使用inline,在外围命名空间Lib中使用using namespace v2;也能将内嵌命名空间v2的成员暴露出来,事实上在C++11以前也确实是这么做的,那么使用内联命名空间还有什么好处呢?主要还有以下两点:
在进行实参依赖查找(argument-dependent lookup, ADL)时,当一个命名空间被添加到关联命名空间集合时,它的内联命名空间也会一起被添加,且当一个内联命名空间被添加到关联命名空间列表时,它的外围命名空间也会一起被添加。
内联命名空间的每个成员,都能按照如同它是外围命名空间的成员一样,进行部分特化、显式实例化或显式特化。
ADL的意思就是在进行函数调用时,除了在无限定名字查找的作用域和命名空间中进行函数名查找,还会在实参所在的命名空间中进行查找,这允许你在进行函数调用时无需显示指定命名空间,更使得使用在不同命名空间中定义的运算符成为可能。下面的例子是正确的:
namespace Lib {
struct Bar1 {};
inline namespace space {
void foo1(Bar1);
struct Bar2 {};
}
void foo2(Bar2);
}
int main() {
Lib::Bar1 bar1;
Lib::Bar2 bar2;
foo1(bar1); // 无需使用Lib::foo1(bar1);
foo2(bar2); // 无需使用Lib::foo2(bar2);
return 0;
}
再来看使用内联命名空间的情况下,用户对内联命名空间中的模板进行特化的例子:
namespace Lib {
inline namespace space {
template<typename T> struct Bar;
}
template<typename T> void foo(T) {}
}
struct UserType {};
namespace Lib {
// 模板特化
template<> struct Bar<UserType> {};
}
int main() {
Lib::Bar<UserType> bar;
foo(bar);
return 0;
}
inline修饰变量
在C++17之后对inline的“允许多次定义”扩充到了变量,使得变量也能像inline函数一样在多个翻译单元中被定义,和inline函数一样,也需要保证每个定义在不同的翻译单元中,以及每处定义都必须是相同的。除此之外,inline还允许在类定义中直接初始化静态成员变量。示例如下:
// foo.hpp
struct Bar {
inline static int val = 1;
};
inline bool foo = true;
// main.cpp
#include "foo.hpp"
int main() {
Bar::val = 2;
foo = false;
return 0;
}
参阅
更多信息,请参阅:
inline 说明符 - cppreference.com
命名空间 - cppreference.com
实参依赖查找 - cppreference.com