内联函数 (C++)

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

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

内联代码替换操作由编译器自行决定。 例如,如果某个函数的地址已被占用或编译器判定函数过大,则编译器不会内联该函数。

类声明的主体中定义的函数是隐式内联函数。

示例

在以下类声明中,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 关键字会重写成本收益分析,改为依赖于程序员的判断。 使用 __forceinline 时应小心谨慎。 不加选择地使用 __forceinline 可能会形成较大代码但是仅获得边际性能提升,或是在某些情况下,甚至会损失性能(例如,由于较大可执行文件的分页会增加)。

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

为了与以前的版本兼容,除非指定了编译器选项 /Za(禁用语言扩展),否则 _inline_forceinline 分别是 __inline__forceinline 的同义词。

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) 编译器选项。

结束 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

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

另请参阅

noinline
auto_inline