何时应该在函数/方法中使用关键字'inline'?

744
什么时候应该在C++中的函数/方法中写关键字inline? 看了一些答案后,还有一些相关的问题:
  • 什么时候不应该在C++中的函数/方法中写关键字'inline'?

  • 编译器什么时候不知道何时将函数/方法设为'inline'?

  • 如果一个应用程序是多线程的,那么在一个函数/方法中写'inline'会有影响吗?


61
如果你在头文件中定义一个函数,你需要声明它为内联函数(inline)。否则,你将会遇到关于函数多重定义的链接器错误。 - Martin York
26
除非它是在一个类定义中,要挑剔的话。 - David Thornley
36
@David:要更加挑剔一些,这只是因为此类函数被隐式标记为 inline(见9.3/2)。 - Lightness Races in Orbit
还可以参考C++ FAQ中的内联函数。它们对内联函数有很好的解释。 - jww
@MartinYork,你甚至可以使用static来避免链接器错误,不是吗? - xyf
16个回答

1163

哦,这是我非常讨厌的事情之一。

inline 更像是 staticextern 而不是指示编译器内联你的函数的指令。 extern, static, inline 是链接指令,几乎只被链接器使用,而不是编译器。

据说 inline 是暗示编译器你认为该函数应该被内联的。这在 1998 年可能是正确的,但十年后编译器不需要这样的提示。更不用说人类通常在优化代码方面是错误的,所以大多数编译器会直接忽略这个“提示”。

  • static - 变量/函数名不能在其他翻译单元中使用。链接器需要确保不会意外使用来自另一个翻译单元的静态定义变量/函数。

  • extern - 在这个翻译单元中使用这个变量/函数名,但不会抱怨它是否已被定义。链接器将解决这个问题,并确保所有尝试使用某些外部符号的代码都有其地址。

  • inline - 此函数将在多个翻译单元中定义,不用担心。链接器需要确保所有翻译单元使用变量/函数的单一实例。

注意:通常,声明模板为 inline 是无用的,因为它们已经具有 inline 的链接语义。但是,模板的显式特化和实例化需要使用 inline


针对你的问题的具体答案:

  • C++ 中什么情况下应该为函数 / 方法编写关键字“inline”?

    仅当您想在头文件中定义函数时才使用。更确切地说,仅当函数的定义可以出现在多个翻译单位中时才这样做。将小型(只有一行代码)函数定义在头文件中是一个好主意,因为它可以为编译器提供更多信息以优化代码。同时,这也会增加编译时间。

  • 在C++中,什么情况下不应该在函数/方法中写入关键字“inline”?

    不要添加内联,仅因为您认为编译器将其内联后代码运行速度更快。

  • 在什么情况下编译器不知道何时使一个函数/方法‘内联’?

    通常,编译器比您更擅长这样做。但是,如果没有函数定义,编译器无法选择将代码内联。在最大程度上优化的代码中,通常会内联所有私有方法,无论您是否要求。

    另外,GCC中防止内联的方法是使用__attribute__(( noinline )),而在Visual Studio中使用__declspec(noinline)

  • 当一个应用程序是多线程的时候,对于函数/方法是否加'内联'关键字有影响吗?

    多线程对内联没有任何影响。


240
我看过的最好的“inline”的描述。现在我会借鉴这个描述,并在我的关于“inline”关键字的所有解释中使用它。 - Martin York
8
这个回答让我有些困惑。你说编译器能更好地内联/不内联代码,然后又说应该将一行代码/小函数放在头文件中,并且编译器无法在没有函数定义的情况下内联代码。这些不是有点矛盾吗?为什么不把所有东西都放在 cpp 文件中,让编译器决定呢? - user673679
10
编译器只会内联函数调用,当定义在调用点处可用时。将所有函数留在 cpp 文件中将限制内联到该文件中。我建议在 .h 中定义小的一行内联函数,编译速度的代价几乎可以忽略不计,并且你几乎可以保证编译器将内联调用。我的观点是,编译器内联是优化黑魔法的一部分,在这方面,你的编译器比你更擅长。 - deft_code
11
每当我阅读到“互联网累积知识”的说法时,我就会想到约翰·劳顿著名的一句话:“信息时代的讽刺是,它让无知的观点变得更有可信度。” - IInspectable
9
“因此,大多数编译器都会完全忽略这个“提示”。”这是明显错误的。至少Clang和GCC使用"inline"关键字作为内联的提示:https://blog.tartanllama.xyz/inline-hints/ - Jean-Michaël Celerier
显示剩余27条评论

116
我想通过一个有说服力的例子来为这个帖子中所有出色的答案做出贡献,以消除任何剩余的误解。 例如,给定两个源文件:
  • inline111.cpp:

    #include <iostream>
    
    void bar();
    
    inline int fun() {
      return 111;
    }
    
    int main() {
      std::cout << "inline111: fun() = " << fun() << ", &fun = " << (void*) &fun;
      bar();
    }
    
  • inline222.cpp:

    #include <iostream>
    
    inline int fun() {
      return 222;
    }
    
    void bar() {
      std::cout << "inline222: fun() = " << fun() << ", &fun = " << (void*) &fun;
    }
    

  • 情况A:

    编译:

    g++ -std=c++11 inline111.cpp inline222.cpp
    

    输出:

    inline111: fun() = 111, &fun = 0x4029a0
    inline222: fun() = 111, &fun = 0x4029a0
    

    讨论:

    1. 即使您应该具有相同的内联函数定义,但如果不是这种情况,C++编译器也不会标记它(实际上,由于分离编译,它没有办法检查)。这是您自己的责任!

    2. 链接器不会抱怨一个定义规则,因为fun()被声明为inline。但是,由于inline111.cpp是第一个被编译器处理的翻译单元(实际上调用fun()),所以编译器在其第一次遇到fun()时实例化它。如果编译器决定从程序中的任何其他地方(例如从inline222.cpp)扩展fun()的调用,则对fun()的调用将始终链接到从inline111.cpp产生的实例(在该翻译单元中对fun()的调用也可能产生一个实例,但它将保持未链接)。确实,这可以从相同的&fun = 0x4029a0打印输出中看出。

    3. 最后,尽管建议编译器实际扩展一行代码fun(),但它完全忽略您的建议,这清楚地表明因为两行都是fun() = 111


  • 情况B:

    编译 (注意反向顺序)

    g++ -std=c++11 inline222.cpp inline111.cpp
    

    输出

    inline111: fun() = 222, &fun = 0x402980
    inline222: fun() = 222, &fun = 0x402980
    

    讨论

    1. 这种情况确认了在情况A中所讨论的内容。

    2. 请注意一个重要点,如果您在inline222.cpp中注释掉对fun()的实际调用(例如完全注释掉inline222.cpp中的cout语句),则尽管翻译单元的编译顺序,fun()将在其在inline111.cpp中首次遇到调用时被实例化,导致情况B的打印输出为inline111: fun() = 111, &fun = 0x402980


  • 案例 C:

    编译 (注意 -O2)

    g++ -std=c++11 -O2 inline222.cpp inline111.cpp
    

    或者

    g++ -std=c++11 -O2 inline111.cpp inline222.cpp
    

    输出

    inline111: fun() = 111, &fun = 0x402900
    inline222: fun() = 222, &fun = 0x402900
    

    讨论

    1. 此处所述-O2 优化会鼓励编译器对于可以进行内联的函数进行实际展开(请注意,没有优化选项时,-fno-inline默认的)。从这里的输出结果可以明显看出,fun() 已经被内联展开了(根据该特定翻译单元中的定义),导致两个不同fun() 打印输出。尽管如此,仍然有只有一个全局链接的 fun() 实例(符合标准要求),正如相同的 &fun 打印输出所表明的。

21
你的回答是一个很好的例子,说明为什么语言会将这样的“inline”函数视为未定义行为。 - R Sahu
你还应该添加编译和链接分开的情况,每个.cpp文件都是自己的翻译单元。最好为启用/禁用-flto的情况添加案例。 - syockit
3
C++参考文献明确表示:“如果内联函数或具有外部链接的变量(自C++17以来)在不同的翻译单元中被不同地定义,行为是未定义的。”因此,你写的内容是针对GCC特定的,因为这是编译和链接过程协调的副作用。此外,请注意,这可能会因版本而异。 - Petr Fiedler
我知道 inline 告诉链接器允许符号冲突(坚持使用第一个翻译单元的符号),但是为什么不需要测试这些符号是否相等呢?标准应该要求编译器为所有内联函数提供 LTO 信息并使这种检查成为强制性的! - Henrik Alsing Friberg

30

如果模板特化在.h文件中,仍然需要显式内联函数。


24

1) 现在几乎没有必要这样做。如果将函数内联是个好主意,编译器会自己完成而不需要你的帮助。

2) 总是。见问题#1。

(编辑以反映您将您的问题分成两个问题...)


1
是的,但这并不太相关 - 要使函数内联,其主体必须在同一编译单元中(例如,在头文件中)。这在C程序中较不常见。 - Michael Kohne
1
定义非成员函数模板(也称为非静态函数模板)不需要内联。请参见一个定义规则(3.2/5)。 - deft_code
5
仍然需要使用 inline 关键字,例如在头文件中定义函数(这是将此类函数内联到多个编译单元中所必需的)。 - Melebius
@Melebius:一个函数可以在多个编译单元中进行内联,而不需要在头文件中定义函数,也不需要使用inline关键字,但编译器必须进行配置(在GCC中称为链接时优化,在Visual Studio中称为整个程序优化)。 - Étienne
3
@Étienne 这是与实现相关的。根据标准,有一个“一次定义规则”,这意味着如果你天真地在多个翻译单元中包含函数定义,你会得到一个错误。但如果该函数具有inline说明符,则链接器会自动将其实例合并为一个,不使用ODR。 - Ruslan
显示剩余2条评论

16

在C++中,什么时候不应该为函数/方法写 'inline' 关键字?

如果该函数在头文件中声明且在 .cpp 文件中定义,则不应该写关键字。

编译器何时不知道将函数/方法设置为 'inline'?

不存在这种情况。编译器无法使函数成为内联函数。它只能内联某些或全部调用该函数。如果编译器没有该函数的代码(在这种情况下,链接器需要在可能的情况下执行此操作),则不能这样做。

在一个多线程的应用程序中,如果在函数/方法中写了 'inline' 关键字,是否会有影响?

不会有任何影响。


有些情况下,在.cpp文件中使用内联是合适的。例如,对完全特定于实现的代码应用优化。 - Robin Davies
@RobinDavies 更新了回答。看起来你误解了我要写的内容。 - Johannes Schaub - litb
@JohannesSchaub-litb 如果函数在头文件中声明并在.cpp文件中定义,则不应使用inline关键字。但是,' deft_code '(967个赞和采纳的答案)提到相反的情况,只有在函数的定义可以出现在多个翻译单位时才应使用inline关键字。因此,我通过在头文件中声明带有inline关键字的函数并在.cpp文件中定义它来检查它,这会导致“未定义的引用”错误,所以你是正确的。现在你还提到……请参见下一个评论。 - Abhishek Mane
@JohannesSchaub-litb ........ 多个翻译单元中的函数代码对编译器不可用,因此它无法将它们内联,这是链接器的工作。在这个意义上,deft_code 表示应该使用 inline 关键字,以便给编译器更多信息来优化代码。因此他的措辞在这里也是有意义的,但当我尝试像之前提到的那样在代码中使用时,会出现错误。所以我感觉你们两个的陈述相互矛盾,但都是有道理的,但当我实际检查时,你的陈述是正确的,所以你能否请解释一下这个问题。 - Abhishek Mane

7
  • 编译器何时无法确定何时将函数/方法“内联”?

这取决于所使用的编译器。不要盲目相信现代编译器比人类更懂得如何进行内联,也不应出于性能原因而使用它,因为它是链接指令而不是优化提示。虽然我同意这些论点在理论上是正确的,但实际情况可能会有所不同。

在阅读了多个线程后,出于好奇,我尝试了一下内联对我正在处理的代码的影响,结果是我在GCC中获得了可测量的加速效果,而在Intel编译器中没有加速效果。

(更多细节:数学模拟与少数关键函数定义在类外部,GCC 4.6.3(g++-O3),ICC 13.1.0(icpc-O3);在关键点添加内联导致GCC代码+6%的加速)。

因此,如果您认为GCC 4.6是现代编译器,那么结果是,如果您编写CPU密集型任务并确切知道瓶颈在哪里,内联指令仍然很重要。


6
我希望看到更多证据支持你的论点。请提供你正在测试的代码以及包含或不包含内联关键字的汇编输出。任何数量的事情都可能带来性能上的好处。 - void.pointer
2
终于有人不只是重复别人的话,而是确实验证了这些说法。GCC确实仍然将inline关键字视为提示(我认为clang完全忽略它)。 - MikeMB
@void.pointer:为什么这么难以置信呢?如果优化器已经完美无缺,那么新版本就不可能提高程序性能。但它们确实经常做到了。 - MikeMB

2
实际上,这种情况非常少见。你所做的只是建议编译器将给定函数内联(例如,用其主体替换对该函数的所有调用)。当然,没有任何保证:编译器可能会忽略该指令。 编译器通常会很好地检测和优化这样的事情。

9
问题在于 inline 在 C++ 中有语义上的差异(例如在处理多个定义的方式上),这在某些情况下非常重要(例如在模板中)。 - Pavel Minaev
4
inline用于解决符号具有多个定义的情况。然而,模板已经被语言处理了。一个例外是不再具有任何模板参数的专门化模板函数(template<>)。这些更像函数而不是模板,因此需要inline关键字来进行链接。 - deft_code

2

如果没有启用优化编译,gcc默认不会内联任何函数。我不知道Visual Studio的情况 - deft_code

我通过使用/FAcs编译并查看汇编代码来检查Visual Studio 9(15.00.30729.01)的情况:

即使在调试模式下,编译器也会生成对成员函数的调用,而没有启用优化。即使该函数标记为__forceinline,也不会生成内联运行时代码。


1
启用 /Wall 以了解哪些函数被标记为内联但实际上没有被内联。 - paulm

1

一个使用情况可能出现在继承中。例如,如果以下所有情况都为真:

  • 你有一个某个类的基类
  • 这个基类需要是抽象的
  • 这个基类除了析构函数之外没有纯虚方法
  • 你不想为基类创建cpp文件,因为没有必要

那么你必须定义析构函数;否则,你将会遇到一些未定义的引用链接错误。此外,你不仅需要定义析构函数,还需要使用inline关键字来定义;否则,你将会遇到多重定义的链接错误。

这可能发生在一些只包含静态方法或编写基本异常类等的辅助类中。

让我们举个例子:

Base.h:

class Base {
public:
    Base(SomeElementType someElement) noexcept : _someElement(std::move(someElement)) {}

    virtual ~Base() = 0;

protected:
    SomeElementType _someElement;
}

inline Base::~Base() = default;

Derived1.h:

#include "Base.h"

class Derived1 : public Base {
public:
    Derived1(SomeElementType someElement) noexcept : Base(std::move(someElement)) {}

    void DoSomething1() const;
}

Derived1.cpp:

#include "Derived1.h"

void Derived1::DoSomething1() const {
    // use _someElement 
}

Derived2.h:

#include "Base.h"

class Derived2 : public Base {
public:
    Derived2(SomeElementType someElement) noexcept : Base(std::move(someElement)) {}

    void DoSomething2() const;
}

Derived2.cpp:

#include "Derived2.h"

void Derived2::DoSomething2() const {
    // use _someElement 
}

通常,抽象类除了构造函数和析构函数外还有一些纯虚方法。因此,在基类中您不必分离虚析构函数的声明和定义,只需在类声明中编写 virtual ~Base() = default; 即可。然而,在我们的情况下并非如此。

据我所知,MSVC允许您在类声明中编写像这样的代码:virtual ~Base() = 0 {}。所以您不需要使用 inline 关键字来分离声明和定义。但是这仅适用于 MSVC 编译器。

现实世界的例子:

BaseException.h:

#pragma once

#include <string>

class BaseException : public std::exception {
public:
    BaseException(std::string message) noexcept : message(std::move(message)) {}
    virtual char const* what() const noexcept { return message.c_str(); }

    virtual ~BaseException() = 0;

private:
    std::string message;
};

inline BaseException::~BaseException() = default;

SomeException.h:

#pragma once

#include "BaseException.h"

class SomeException : public BaseException {
public:
    SomeException(std::string message) noexcept : BaseException(std::move(message)) {}
};

SomeOtherException.h:

#pragma once

#include "BaseException.h"

class SomeOtherException : public BaseException {
public:
    SomeOtherException(std::string message) noexcept : BaseException(std::move(message)) {}
};

main.cpp:

#include <SomeException.h>
#include <SomeOtherException.h>

#include <iostream>

using namespace std;

static int DoSomething(int argc) {
    try {
        switch (argc) {
        case 0:
            throw SomeException("some");
        case 1:
            throw SomeOtherException("some other");
        default:
            return 0;
        }
    }
    catch (const exception& ex) {
        cout << ex.what() << endl;
        return 1;
    }
}

int main(int argc, char**) {
    return DoSomething(argc);
}

1

F.5: 如果一个函数非常小且时间关键,声明为内联函数。

原因:一些优化器擅长在没有程序员提示的情况下进行内联,但不要依赖它。测量!在过去的40年左右的时间里,我们一直期望编译器可以在没有人类提示的情况下比人类更好地进行内联。我们仍在等待。明确指定内联(显式或隐式地在类定义中编写成员函数时)鼓励编译器做得更好。

来源:https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#Rf-inline

有关示例和异常,请参见源代码(请参见上文)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,