《C++之那些年踩过的坑(二)》

C++之那些年踩过的(二)

作者:刘俊延(Alinshans)

本系列文章针对我在写C++代码的过程中,尤其是做自己的项目时,踩过的各种坑。以此作为给自己的警惕。

 


转载请注明一下原文来自 : http://www.cnblogs.com/GodA/p/6554591.html 

 

博客园的编译好渣,我用 Markdown 重写了一遍,内容也作了修正和调整,请移步:https://alinshans.github.io/2017/05/23/p1705231/

 

第一次修改:2017/3/26

发表于     : 2017/3/15

 

今天讲一个小点,虽然小,但如果没有真正理解它,没有真正熟悉它的里里外外,是很容易出错的 —— inline

关于一些简单的介绍和使用,可以先看我 这篇笔记 。接下来进入正题。

 

一、如何使用 inline?

你知道,inline 函数可以减小函数调用的开销,你可能会想,嗯,我这个函数那么短,我把它声明为 inline,可以提高程序运行的效率!考虑这样一个例子:

// A.h
#include <cstdio> class A { public: void foo(int i); void bar(int i) { std::printf("%d\n", i + 1); } };
// A.cc
#include "A.h" void A::foo(int i) { std::printf("%d\n", i); }
// main.cc
#include "A.h"
int main()
{
  A a;
  a.foo(1);
  a.bar(1);
}

 首先,你知道,①inline 需要看到函数实体,所以要跟定义放在一起。于是你想在 A.cc 中在为 foo 的定义加上一个 inline :

inline void A::foo(int i)

然后开心的编译运行,WTF!!!编译器居然报错了?!!不就加了个 inline 吗!仔细观察编译器给的出错信息,如果你用的是VS,那么你大概会看到这样的信息: error LNK2019: 无法解析的外部符号……如果你用的是GCC,你会发现当你使用

g++ -c main.cc

 时(即编译),是不会产生任何错误的,然后当你使用

g++ main.o -o a.out

时(即链接),就报错了。说明,这是链接的时候出错了。在这里要说明一下,大多数的建置环境都是在编译过程进行 inlining(为了替换函数调用,编译器需要知道函数的实体长什么样,这解释了①),某些可以在连接期完成,少数的可以在运行期完成。我们只考虑绝大部分情况:Inlining 在大多数C++程序中是编译期行为。

大部分函数默认的就是外部链接,也就是外部可以访问,而 inline 函数默认具有内部链接,也就是对本文件可见,对其它文件不可见。那么自然我们在 main.cc 中调用它,没法看到它的定义,于是就出现了连接错误。OK,你学到了 ②一般 inline 需要放在头文件中

首先你要先了解一下内部链接与外部链接,可以看这里。它提到:

names of classes, their member functions, static data members (const or not), nested classes and enumerations, and functions first introduced with friend declarations inside class bodies

ok,类的成员函数是具有外部链接的,然后我们看这里它提到:

An inline function or variable (since C++17) with external linkage (e.g. not declared static) has the following additional properties:

1) It must be declared inline in every translation unit.
2) It has the same address in every translation unit.

嗯,意思很明白了,就是如果一个函数,是外部链接的,你给它搞成 inline 了,那么,请你在每一个编译单元都做一个 inline 定义。也就是说,如果你想让上面的代码运行,没问题,那请把 main.cpp 改成这样:

// main.cpp
#include <iostream>
#include "A.h"

class A;
inline void A::foo(int i)
{
  std::printf("%d\n", i);
}

int main()
{
  A a;
  a.foo(1);
}

  我想,如果有十个编译单元要引用它呢?一百个呢?你可能不愿意这样写。而在这里开头还有提到:

A function defined entirely inside a class/struct/union definition, whether it's a member function or a non-member friend function, is implicitly an inline function.

  在类内定义的成员函数,是自动 inline 的,不需要你去加,LLVM CodingStandards 也是这样提出的

那你可能会想马上想到还有一种情况:如果一个类成员函数,既不定义在类内,也不定义在编译单元,而是定义在头文件,并且在类外,这种情况,又会发生什么呢?也就是这样:

// A.h
#include <cstdio>
class A
{
public:
  void foo(int i);
  void bar(int i);
};

inline void A::bar(int i)
{
  std::printf("%d", i + 1);
}

  嗯,可以,这样写通过编译,并且可以运行了。不过,它如你所想提高效率了吗?我们可以探究一下。在vs下可以用调试看反汇编,现在用GCC分别运行以下命令:

g++ -E main.cc -o main.i
g++ -S main.i -o main.s
g++ -O2 -S main.i -o main2.s

 我们来看一下 main.s 中的主要部分:

    call    ___main
    leal    -9(%ebp), %eax
    movl    $1, (%esp)
    movl    %eax, %ecx
    call    __ZN1A3fooEi
    subl    $4, %esp
    leal    -9(%ebp), %eax
    movl    $1, (%esp)
    movl    %eax, %ecx
    call    __ZN1A3barEi
    subl    $4, %esp
    movl    $0, %eax
    movl    -4(%ebp), %ecx

我们再看一下 main2.s 中的这个部分:

    call    ___main
    leal    -9(%ebp), %ecx
    movl    $1, (%esp)
    call    __ZN1A3fooEi
    subl    $4, %esp
    movl    $2, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movl    -4(%ebp), %ecx

在不开优化的情况下,程序诚实的执行,开O2优化的情况下,我们已经看不到 bar 函数的调用了。不过这真的是拜你加的 inline 所赐的吗?为了验证,我们去掉 inline,打算再次重复上面的过程,然后你就会发现,WTF!!!编译器又报错了??发生了什么??

 

二、什么时候应该使用 inline?

嗯,终于我们来到了第二个问题,我们发现,当我们给函数去掉 inline 时,居然无法通过编译了!它给出来的错误信息是:重定义的符号。让我们冷静下来,想一想,然后你就会恍然大悟:一个函数可以有多次声明,但只能有一次定义,而我们定义在 A.h 的 bar 函数的定义,被 A.cc 和 main.cc 都包含了一遍!所以就出现了重定义的错误!是的是的,我也想不到有什么理由让一个类成员函数的定义即不出现在类内部,也不出现在编译单元,除非是模板类成员函数/类模板成员函数。不过你现在应该对 inline 与类成员函数的种种事情,有了非常清晰的认识了。即 ②不要把 inline 用在类的成员函数上当然,也别写出上面那种情况的代码。

然后我们来看看 inline 跟普通函数结合的情况。这种情况,更容易被我们忽视,例如,我们想在 A.h 中加一个函数:

// A.h
int
max(int a, int b) { return a < b ? b : a; }

它很短,要不要使用 inline 呢?经过刚刚的问题,你应该会谨慎的想到,这里,要使用 inline ,如果不使用,就会出错。原因跟上面提到的是一样的。当然,它不是非得使用 inline 不可,你可以把它的函数定义放在源文件,就不会有重复定义的问题。甚至你也可以在头文件定义并且使用 static 修饰它,也可以解决问题,这个就不展开了。

但当你用上模板时,情况发生了改变。若你把这个 max 函数改成一个模板函数:

template <typename T>
T max(const T& a, const T& b)
{
  return a < b ? b : a;
}

这个时候,无论你有没有使用 inline,它都是可以运行的。这是因为,模板是具有“内联”语义的。所以,类模板,函数模板,类函数模板,都不需要加 inline 。回到正题,什么时候可以使用 inline 呢?③使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时

 

三、inline 可以提升程序运行效率?

我们刚刚还没有完成我们的实验,我还没有打消你使用 inline 去“优化”程序的念头。所以,让我们再次做一次实验,这次为了方便,我在 main.cc 定义一个函数:

 

// main.cc
#include <cstdio>

int test(int i)
{
  i = i + 1;
  return i;
}

int main()
{
  std::printf("%d\n", test(1));
}

 

  这样的短代码,你想优化了是吧?先别急,我们就这样,编译汇编看看,运行同样的命令:

g++ -E main.cc -o main.i
g++ -S main.i -o main.s
g++ -O2 -S main.i -o main2.s

  然后看 main.s (未开优化)的主要部分:

    call    ___main
    movl    $1, (%esp)
    call    __Z4testi
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf

  然后再看看 main2.s(开O2优化)的这个部分:

    call    ___main
    movl    $2, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf

  嗯是的没错,你没有声明 inline,但是编译器的优化,帮你把这个函数内联展开了,所以在 main2.s 中看不到 test 的调用了。你加上 inline 重复这个过程,还是会得到一样的结果。还没有死心吗?你是不是想说,这个函数太简单了,我是编译器我都看得出来可以优化啊!听说复杂一点编译器就不会优化了!比如函数里面有循环,递归什么的!

  好,于是你改成这样:

// main.cc
#include <cstdio>

int test(int i)
{
  int x = 0;
  for (int j = 0; j < i; ++j)
  {
    x += j;
  }
  return x;
}

int main()
{
  std::printf("%d\n", test(100));
}

  再次编译汇编,你猜猜你会看到什么?好吧,我只把 main2.s 中的那个部分给你看看:

    call    ___main
    movl    $4950, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf

  你还想说什么吗?如果还没死心,请继续尝试其他情况。我不会帮你试,不过我可以帮你试试这个情况:

// main.cc
#include <cstdio>
#include <cmath>

inline int test(int i)
{
  int prime[100];
  int k = 0;
  for (int n = 2; n <= i; ++n)
  {
    bool is_prime = true;
    for (int j = 2; j <= static_cast<int>(std::sqrt(n)); ++j)
    {
      if (n % j == 0)
      {
        is_prime = false;
        break;
      }
    }
    if (is_prime)
    {
      prime[k] = n;
      ++k;
    }
  }
  int sum = 0;
  for (int n = 0; n < k; ++n)
  {
    sum += prime[n];
  }
  return sum;
}

int main()
{
  std::printf("%d\n", test(100));
}

  嗯。。长是长了点,但是你声明了一个 inline 呀!好吧,我们再看看生成的两份汇编代码:

  main.s:

    call    ___main
    movl    $100, (%esp)
    call    __Z4testi
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    movl    $0, %eax

  main2.s:

    call    ___main
    movl    $100, (%esp)
    call    __Z4testi
    movl    $LC2, (%esp)
    movl    %eax, 4(%esp)
    call    _printf
    xorl    %eax, %eax

  这一次,无论是否开优化,都调用了 test。然后你很无奈的发现,编译器是否选择内联,跟你声不声明没有半毛钱关系啊!!

 

 

四、 inline 的真正意义?

现在你该好好的思考,什么是 inline,是内联吗?inline 的意义是什么,是发起一个内联请求吗?

你认为加 inline 是为了提高程序的运行效率,但是事实上,并不会跟 inline 有什么关系啊。但有的时候,你不加 inline,却会出错。这跟“内联”两个字,好像已经没什么关系了?

好好的思考一下吧。

 

 

 

 

 

这么快就往下看了,花点时间在思考一下?

 

 

 

 

 

好吧。

inline,跟 static , extern 一样,都是链接指令,它在很久很久以前,是作为给编译器优化的提示符。而 inline 的含义是非绑定的,编译器可以自由的选择、决定是否 inline 一个函数。如今,编译器根本不需要这样的提示,如果它认为一个函数值得 inline,它会自动 inline,否则,即使你 inline 了,它也会拒绝。如果你仔细阅读 http://en.cppreference.com/w/cpp/language/inline 的话,尤其是其中的 Desription:

 

Description

 

An inline function or inline variable (since C++17) is a function or variable (since C++17) with the following properties:

 

1) There may be more than one definition of an inline function or variable (since C++17) in the program as long as each definition appears in a different translation unit and (for non-static inline functions) all definitions are identical. For example, an inline function or an inline variable (since C++17) may be defined in a header file that is #include'd in multiple source files.

 

2) The definition of an inline function or variable (since C++17) must be present in the translation unit where it is accessed (not necessarily before the point of access).

 

3) An inline function or variable (since C++17) with external linkage (e.g. not declared static) has the following additional properties:

 

1) It must be declared inline in every translation unit.

 

2) It has the same address in every translation unit.

 

你会发现,全文几乎没有提到 “优化代码”、“减小开销” 等等字眼。而你在网上所搜素到的关于 inline 的信息,几乎都告诉你,inline 可以怎么怎么优化。要么用了假的搜索引擎,要么看了假网页 ,要么…… 在这篇 SO 中,有一段话:

It is said that inline hints to the compiler that you think the function should be inlined. That may have been true in 1998, but a decade later the compiler needs no such hints. Not to mention humans are usually wrong when it comes to optimizing code, so most compilers flat out ignore the 'hint'.

  • static - the variable/function name cannot be used in other compilation units. Linker needs to make sure it doesn't accidentally use a statically defined variable/function from another compilation unit.

  • extern - use this variable/function name in this compilation unit but don't complain if it isn't defined. The linker will sort it out and make sure all the code that tried to use some extern symbol has its address.

  • inline - this function will be defined in multiple compilation units, don't worry about it. The linker needs to make sure all compilation units use a single instance of the variable/function.

看完你应该差不多能理解了。现在的编译器,并不需要你用 inline 提醒,所以,当且仅当你认为使用 inline 会加快程序运行效率时,不要使用 inline 。inline 这个关键字,在C++里就是一个骗局。它真正的意义并不是去内联一个函数,而是表示 别怕!无论你看到了多少个定义,但实体就我一个!  Reference 中有有这样一句话:

Because the meaning of the keyword inline for functions came to mean "multiple definitions are permitted" rather than "inlining is preferred", that meaning was extended to variables.

翻译过来就是 ④ inline 的含义更多的是 “允许多重定义” 而不是 “优先选择内联函数” 。全文基于 C++17 及以前的讨论。

 

 五、总结

 

1、inline 需要看到函数实体,所以要跟定义放在一起

2、不要把 inline 用在类的成员函数上

3、使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时

4、inline 的含义更多的是 “允许多重定义” 而不是 “优先选择内联函数”

5、模板不需要声明 inline,也具有 inline 的语义

 

※注:以上总结适用于不熟悉、不了解 inline 的同学。若对以上内容都了解,使用 inline 的时候,很明白很清楚在做什么,会发生什么,那就随便怎么用啦!

 

posted on 2017-03-15 15:44  Alinshans  阅读(1637)  评论(0编辑  收藏  举报