现代 C++ 编译器是否能够在某些情况下避免两次调用 const 函数?

Are modern C++ compilers able to avoid calling a const function twice under some conditions?

例如,如果我有这个代码:

class SomeDataProcessor
{
public:
    bool calc(const SomeData & d1, const SomeData & d2) const;
private:
    //Some non-mutable, non-static member variables
}

SomeDataProcessor sdp;
SomeData data1;
SomeData data2;

someObscureFunction(sdp.calc(data1, data2),
                    sdp.calc(data1, data2));

让我们考虑可能 等效代码:

bool b = sdp.calc(data1, data2);
someObscureFunction(b,b);

为了使这个有效,calc() 函数应该满足一些要求,例如我调用 属性 _pure_const_formula_

A _pure_const_formula_ 会:

例如,调用随机数生成器不符合这些要求。

是否允许编译器用第二个代码替换第一个代码,即使它需要递归地挖掘被调用的函数?现代编译器能够做到这一点吗?

不,鉴于显示的代码,编译器无法保证建议的优化没有可观察到的差异,并且现代编译器无法优化第二个函数调用。

一个非常简单的示例:此 class 方法可能会使用随机数生成器,并将结果保存在某个私有缓冲区中,稍后代码的其他部分会读取该缓冲区。显然,现在消除函数调用会导致更少的 randomly-generated 值被放置在该缓冲区中。

换句话说,仅仅因为 class 方法是 const 并不意味着它在调用时没有明显的副作用。

不,编译器不允许在这种情况下这样做。 const 仅表示您不更改该方法所属对象的状态。但是,使用相同的输入参数多次调用此方法可能会产生不同的结果。例如,考虑一种产生随机结果的方法。

GCC 的函数有 pure attribute(用作 __attribute__((pure))),它告诉编译器可以消除冗余调用。它用于例如在 strlen 上。

我不知道有任何编译器会自动执行此操作,尤其是考虑到以下事实:要调用的函数可能无法以源形式提供,并且目标文件格式不包含有关函数是纯函数还是纯函数的元数据不是。

是的,绝对。

Compilers do this all the time, and more.

例如,如果您的所有函数所做的都是 return true,并且它的定义在调用点对编译器可见,则整个函数调用可能会被省略,结果只是:

someObscureFunction(true, true);

编译器拥有足够信息的程序可能 "optimised" 从相当复杂的任务链到可能只有一两条指令。现在,实际上对成员变量的操作在某种程度上将优化器推到了它的极限,但是如果变量是 private,被赋予一个已知的初始值,并且没有被任何其他成员函数改变,我不会了解为什么编译器不能根据需要内联其已知值。编译器非常非常聪明。

人们认为编译后的程序是源代码中行的 one-to-one 映射,但事实并非如此。 C++ 的全部目的是它是计算机运行程序时实际要执行的操作的抽象

是的,现代 C 编译器可以消除冗余函数调用 当且仅当 他们可以证明这样的优化行为 如同 遵循原始程序语义。例如,这意味着如果函数没有副作用并且其 return 值仅取决于参数,则他们可以消除对具有相同参数的同一函数的多次调用。

现在,您专门询问了 const - 这对开发人员而非编码人员非常有用。 const 函数是一个 提示 ,该方法不会修改调用它的对象,const 个参数是 提示 参数没有被修改。但是,该函数可能(合法地1)丢弃 this 指针或其参数的 const-ness。所以编译器不能依赖它。

此外,即使传递给函数的 const 个对象在该函数中确实从未被修改过,并且 const 个函数从未修改过接收者对象,该方法也可以很容易地依赖可变的全局数据(并可能改变这些数据)。例如,考虑一个 returns 当前时间或递增全局计数器的函数。

所以 const 声明帮助了程序员,而不是编译器2

但是,编译器可能会使用其他技巧来证明调用是多余的:

  • 该函数可能与调用者在同一个编译单元中,允许编译器检查它并确定它到底依赖什么。其最终形式是内联:函数体可能会被移动到调用者中,此时优化器可以从以后的调用中删除冗余代码(直到完全包括来自这些调用的所有代码,也许是原始调用的全部或端口也是)。
  • 工具链可能会使用某种类型的 link-time-optimization,即使对于不同编译单元中的函数和调用者,它也能有效地允许上一点中描述的分析类型。这可以允许在生成最终可执行文件时对 任何 代码进行优化。
  • 编译器可能允许用户使用一个属性来注释一个函数,该属性通知编译器它可以将该函数视为没有副作用。例如,gcc 提供了 pureconst 函数属性,这些属性通知 gcc 这些函数没有副作用,并且只依赖于它们的参数(和全局变量,在pure).

1 通常,只要对象不是最初定义为 const.

2 从某种意义上说 const 定义 确实有助于编译器:它们可以将全局对象定义为 const 到可执行文件的只读部分(如果存在这样的功能),并在它们相等时组合这些对象(例如,相同的字符串常量)。