2

在我的实现文件(.cc 文件)中,我经常发现在类定义中定义成员函数(例如 Pimpl 类的函数)很方便。例如:

struct X::Impl {
    void DoSomething() {
        ...
    }
};

代替

struct X::Impl {
    void DoSomething();
};

void X::Impl::DoSomething() {
    ...
}

我认为这比在类定义之外实现函数更可取,原因有几个。它增强了可读性并促进了保持方法小(通过使其易于添加)的实践。代码也更容易维护,因为您不必更新方法声明。

我看到的唯一缺点是类声明中定义的方法是隐式内联的,由于目标代码大小的增加,这通常是不可取的。

我的问题是:

  1. 我有这个权利吗?我缺少这种做法的其他缺点吗?

  2. 隐式内联是否值得担心?编译器是否足够聪明,可以拒绝我对不应内联的内联方法的隐式请求?

  3. 是否可以(通过编译器扩展或其他方式)声明类定义中定义的方法不被内联?

4

4 回答 4

3

简单的答案是你不应该关心。在类定义中定义的成员函数是隐式的inline,但这并不意味着它们是内联的(即代码不需要在调用处内联)。

编译器实现者花费了大量时间和资源来提出启发式方法,根据函数的大小、复杂性以及是否可以内联(递归函数不能内联[*])。与我们大多数人相比,编译器拥有更多关于生成代码及其运行架构的信息。相信它,然后如果您觉得可能存在问题,配置文件,并且如果配置文件表明您应该更改代码,请执行此操作,但在事后做出明智的决定。

如果你想验证函数是否真的被内联了,你可以查看程序集并检查是否有对函数的调用或者代码是否真的被内联了。


[*]如果编译器可以将递归转换为迭代,就像尾递归的情况一样,那么理论上可以内联转换后的函数。但是,带有循环的函数无论如何被内联的可能性较小......

于 2012-06-01T23:40:53.260 回答
2

我不知道防止内联的可移植方式,但是使用 GCC 你可以使用属性。例如:

struct X::Impl {
    void DoSomething() __attribute__((noinline)) {
        ...
    }
};
于 2012-06-01T23:23:31.260 回答
0

将您的内联选择所节省的所有 CPU 时间加起来。在他们身上花的钱比你自己的少。线性路径长度担忧适用于现在每天处理数万亿次执行的路径。在 L1 中,40 亿已经足够接近一秒了。250 次不是五分钟。如果没有人抱怨性能,那么您和您的编译器的选择至少在正确的联盟中发挥作用。

关于可读性的意见大相径庭。重要的是你的听众的意见。

于 2012-06-01T23:44:57.117 回答
0

一般来说,编译器会随心所欲地内联,不会过多关注inline程序员给出的任何关键字。inline通常被视为或多或少与static.

例如,递归函数通常不能完全内联(您要内联的函数体将再次包含应该内联的调用)。为了测试你的特定编译器的行为,你可以定义一个这样的类:

class Fib {
public:      
   int calc(int i);
};

int Fib::calc(int i) {
  if (i > 1)
     return calc(i-1) + calc(i-2);
  return 1;
}

有了这个定义,如果编译器觉得有义务内联所有对 的调用,则以下示例程序将无法在合理的时间或大小内编译calc

#include "tst.hpp"
#include <iostream>                
int main() {
   Fib f;
   std::cout << f.calc(1000) << std::endl;
};

所以你可以通过编译这个程序来测试你的编译器行为。如果编译成功,编译器不会内联所有对Fib::calc.

于 2012-06-01T23:46:58.510 回答