通过


内联函数 (C++)

关键字 inline 建议编译器使用函数定义中的代码替换对该函数的每次调用。

理论上,使用内联函数可以加速程序运行,因为它们消除了与函数调用关联的开销。 调用函数需要将返回地址推送到堆栈、将参数推送到堆栈、跳转到函数体,然后在函数完成时执行返回指令。 通过内联函数可以消除此过程。 相对于未内联扩展的函数,编译器还有其他机会来优化内联扩展的函数。 内联函数的一个缺点是程序的整体大小可能会增加。

内联代码替换操作由编译器自行决定。 例如,如果编译器的地址被采用,或者编译器决定函数太大,则编译器不会内联函数。

关键字 inline 和一个定义规则 (ODR)

原始含义 inline 是编译器倾向于在调用站点上扩展代码而不是函数调用指令的提示。 这仍然是其中一个 inline含义。

但是,关键字 inline 也对一个定义规则(ODR)有影响。 通常,函数只能在所有翻译单元中定义一次。 标记 inline函数时,如果所有定义都相同,则可以在多个转换单元(通常通过头文件)中定义它。 然后,链接器选择一个定义并放弃重复项,而不是报告错误。

这种双重性质 inline(作为优化提示和 ODR 机制)可能会导致混淆。 ODR 方面是一种实际需要,其中同一标头(包含内联函数定义)可以包含在多个源文件中。

隐式内联函数

某些函数隐 inline 式不需要关键字:

  • 在类范围定义的函数:类声明正文中定义的函数隐式为内联函数。 这允许小型访问器函数直接在类定义中定义,而不会产生函数调用开销,这是自 C++ 早期以来的优先级。

  • constexpr 函数:声明 constexpr 的函数(在 C++11 中引入)是 inline隐式的。 由于 constexpr 函数通常在头文件中定义以允许编译时计算,因此它们必须遵循与内联函数相同的 ODR 规则。

  • consteval 函数:声明 consteval 的函数(在 C++20 中引入)是 inline隐式的。

内联变量 (C++17)

C++17 将 inline 关键字扩展到变量。 inline可以在多个转换单元(如内联函数)中定义变量,链接器选择一个定义并放弃其余部分。

内联变量可用于在头文件中定义常量或静态数据成员:

// constants.h
inline constexpr double pi = 3.14159265358979323846;

struct MyClass
{
    static inline int instanceCount = 0;  // Can be defined in header
};

在 C++17 之前,此类变量需要在单个源文件中单独定义以避免链接器错误。

示例:内联类成员函数

在以下类声明中, Account 构造函数是内联函数,因为它在类声明的正文中定义。 成员函数 GetBalanceDepositWithdraw 是在其定义中指定的 inline。 关键字 inline 在类声明中的函数声明中是可选的。

// account.h
class Account
{
public:
    Account(double initial_balance)
    {
        balance = initial_balance;
    }

    double GetBalance() const;
    double Deposit(double amount);
    double Withdraw(double amount);

private:
    double balance;
};

inline double Account::GetBalance() const
{
    return balance;
}

inline double Account::Deposit(double amount)
{
    balance += amount;
    return balance;
}

inline double Account::Withdraw(double amount)
{
    balance -= amount;
    return balance;
}

注意

在类声明中,声明函数而无需 inline 关键字。 inline 关键字可以在类声明中指定;结果也相同。

给定的内联成员函数在每个编译单元中必须以相同的方式进行声明。 内联函数必须只有一个定义。

除非该函数的定义包含 inline 说明符,否则类成员函数默认为外部链接。 前面的示例显示,无需使用 inline 说明符显式声明这些函数。 在 inline 函数定义中使用建议编译器将其视为内联函数。 但是,无法在调用该函数后将函数重新声明为 inline

inline__inline__forceinline

inline__inline 说明符建议编译器将函数体的副本插入到调用函数的每个位置。

仅当编译器自己的成本收益分析显示有价值时,才会进行插入(称为内联展开内联)。 内联展开以代码大小较大的潜在成本最大程度地减少函数调用开销。

关键字 __forceinline (或 [msvc::forceinline] 属性)替代了成本效益分析,并依赖于程序员的判断。 使用 __forceinline 时应小心谨慎。 不加选择地使用 __forceinline 可能会形成较大代码但是仅获得边际性能提升,或是在某些情况下,甚至会损失性能(例如,由于较大可执行文件的分页会增加)。

编译器将内联扩展选项和关键字视为建议。 不保证会对函数进行内联。 无法强制编译器对特定函数进行内联(即使使用 __forceinline 关键字)。 使用 /clr 进行编译时,如果对函数应用了安全特性,则编译器不会对函数进行内联。

为了与以前的版本兼容,_inline并且_forceinline是两个前导下划线的同义词__inline__forceinline ,除非指定编译器选项/Za(禁用语言扩展)。

inline 关键字告知编译器,内联展开是首选操作。 但编译器可以忽略它。 可能会出现这种行为的两种情况是:

  • 递归函数。
  • 在翻译单元中的其他位置通过指针引用的函数。

和编译器确定的其他原因一样,这些原因会干扰内联。 不要依赖于 inline 说明符来导致内联某个函数。

编译器可以在多个翻译单元中将头文件中定义的内联函数作为一个可调用函数来创建,而不是扩展它。 编译器为链接器标记生成的函数,防止出现单一定义规则 (ODR) 冲突。

与普通函数一样,内联函数中的自变量计算没有明确顺序。 事实上,这可能与使用普通函数调用协议传递时的自变量计算顺序不同。

/Ob 编译器优化选项来影响是否实际进行内联函数展开。
/LTCG 是否在源代码中请求跨模块内联。

示例 1

// inline_keyword1.cpp
// compile with: /c
inline int max(int a, int b)
{
    return a < b ? b : a;
}

可以通过使用 inline 关键字或通过将函数定义置于类定义中,来内联声明类的成员函数。

示例 2

// inline_keyword2.cpp
// compile with: /EHsc /c
#include <iostream>

class MyClass
{
public:
    void print() { std::cout << i; }   // Implicitly inline

private:
    int i;
};

Microsoft 专用

__inline 关键字等效于 inline

如果出现以下情况,则即使 __forceinline,编译器也无法内联函数:

  • 函数或其调用方是使用 /Ob0(调试生成的默认选项)编译的。
  • 函数和调用方使用不同类型的异常处理(一个中使用 C++ 异常处理,另一个中使用结构化异常处理)。
  • 函数具有变量自变量列表。
  • 除非使用 /Ox/O1/O2 进行编译,否则函数使用内联程序集。
  • 该函数是递归函数,不设置 #pragma inline_recursion(on)。 递归函数使用杂注内联为 16 个调用的默认深度。 若要减小内联深度,请使用 inline_depth pragma。
  • 函数是虚函数,进行虚拟调用。 对虚函数的直接调用可以进行内联。
  • 程序采用函数地址,调用通过指向函数的指针进行。 对采用其地址的函数的直接调用可以进行内联。
  • 函数还使用 naked__declspec 修饰符进行标记。

如果编译器无法对使用 __forceinline 声明的函数进行内联,它会生成级别 1 警告,但以下情况除外:

  • 该函数是使用 /Od 或 /Ob0 编译的。 在这些情况下不需要进行内联。
  • 该函数是在外部定义的,位于包含的库或其他翻译单元中,或者是虚拟调用目标或间接调用目标。 编译器无法识别在当前翻译单元中找不到的非内联代码。

递归函数可以用内联代码替换为 inline_depth pragma 指定的深度,最多 16 个调用。 该深度之后,递归函数调用被视为对函数实例的调用。 内联启发式方法对递归函数检查到的深度不能超过 16。 inline_recursion pragma 控制当前进行展开的函数的内联展开。 若要了解相关信息,请参阅内联函数展开 (/Ob) 编译器选项。

C++ 标准定义了一组通用属性。 它还允许编译器供应商在特定于供应商(在本例中) msvc命名空间中定义自己的属性。 以下特定于Microsoft属性可用于控制内联行为:

用于控制内联行为的Microsoft特定属性

Attribute Meaning
[msvc::forceinline] 具有与 __forceinline.
[msvc::forceinline_calls] 可以置于语句或块之前,导致内联启发式强制内联在该语句或块中的所有调用。
[msvc::flatten] 类似于 [[msvc::forceinline_calls]],但递归强制内联在应用范围中的所有调用,直到没有留下任何调用。
[msvc::noinline] 在函数声明之前放置时,的含义与 __declspec(noinline).
[msvc::noinline_calls] 可以在任何语句或块之前放置,以关闭应用于该语句的范围中的所有调用的内联。

结束 Microsoft 专用

有关使用 inline 说明符的详细信息,请参阅:

何时使用内联函数

内联函数最适合用于小型函数,例如提供数据成员访问权限的函数。 短函数对函数调用的开销很敏感。 较长的函数在调用和返回序列方面花费的时间可成比例地减少,而从内联的获益也会减少。

Point 类可以定义如下:

// when_to_use_inline_functions.cpp
// compile with: /c
class Point
{
public:
    // Define "accessor" functions
    // as reference types.
    unsigned& x();
    unsigned& y();

private:
    unsigned _x;
    unsigned _y;
};

inline unsigned& Point::x()
{
    return _x;
}

inline unsigned& Point::y()
{
    return _y;
}

假设坐标操作是此类客户端中相对常见的操作,则将两个访问器函数(前面示例中的 xy)指定为 inline 通常将节省下列操作的开销:

  • 函数调用(包括参数传递和在堆栈上放置对象地址)
  • 保留调用者的堆栈帧
  • 设置新的堆栈帧
  • 返回值通信
  • 还原旧堆栈帧
  • 退回

内联函数与宏

宏与 inline 函数之间有一些共同之处。 但存在两个重要的差异。 请考虑以下示例:

#include <iostream>

#define mult1(a, b) a * b
#define mult2(a, b) (a) * (b)
#define mult3(a, b) ((a) * (b))

inline int multiply(int a, int b)
{
    return a * b;
}

int main()
{
    std::cout << (48 / mult1(2 + 2, 3 + 3)) << std::endl; // outputs 33
    std::cout << (48 / mult2(2 + 2, 3 + 3)) << std::endl; // outputs 72
    std::cout << (48 / mult3(2 + 2, 3 + 3)) << std::endl; // outputs 2
    std::cout << (48 / multiply(2 + 2, 3 + 3)) << std::endl; // outputs 2

    std::cout << mult3(2, 2.2) << std::endl; // no warning
    std::cout << multiply(2, 2.2); // Warning C4244	'argument': conversion from 'double' to 'int', possible loss of data
}
33
72
2
2
4.4
4

下面是宏与内联函数之间的一些差异:

  • 宏始终是内联扩展的。 但是,仅当编译器确定它是最佳作时,内联函数才会内联。
  • 宏可能会导致意外行为,从而导致微小的 bug。 例如,表达式 mult1(2 + 2, 3 + 3) 展开为 2 + 2 * 3 + 3,其计算结果为 11,但预期结果为 24。 看似有效的修复是围绕函数宏的两个参数添加括号,从而 #define mult2(a, b) (a) * (b)解决了手头的问题,但在较大表达式的一部分时仍可能导致令人惊讶的行为。 这种情况已在前面的示例中进行了演示,可以通过将宏定义为 #define mult3(a, b) ((a) * (b)) 来解决该问题。
  • 内联函数受编译器的语义处理约束,而预处理器会扩展宏,但没有任何好处。 宏不是类型安全的,而函数是。
  • 计算一次作为内联函数的参数传递的表达式。 在某些情况下,作为宏的自变量传递的表达式可计算多次。 例如,请考虑如下事项:
#include <iostream>

#define sqr(a) ((a) * (a))

int increment(int& number)
{
    return number++;
}

inline int square(int a)
{
    return a * a;
}

int main()
{
    int c = 5;
    std::cout << sqr(increment(c)) << std::endl; // outputs 30
    std::cout << c << std::endl; // outputs 7

    c = 5;
    std::cout << square(increment(c)) << std::endl; // outputs 25
    std::cout << c; // outputs 6
}
30
7
25
6

在此示例中,在表达式 increment 展开为 sqr(increment(c)) 时调用两次函数 ((increment(c)) * (increment(c)))。 这导致第二次调用 increment 时返回 6,因此表达式的计算结果为 30。 任何包含副作用的表达式在宏中使用时都可能会影响结果,请检查完全展开的宏以检查该行为是否符合预期。 而当使用内联函数 square 时,将只调用一次 increment 函数,并得到正确结果 25。

另请参阅

noinline
auto_inline
[msvc::forceinline]
[msvc::forceinline_calls]
[msvc::flatten]
[msvc::nolinline]
[msvc::nolinline_calls]