前言

一些教程和文章里对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

文章作者: Liccsu
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Liccsu's blog
喜欢就支持一下吧
打赏
微信 微信