为什么不允许常量子表达式消除const非易失性成员函数?

时间:2014-05-18 13:46:04

标签: c++ c++11 compiler-optimization

C ++的目标之一是允许用户定义的类型与内置类型一样好。这似乎失败的一个地方是编译器优化。如果我们假设const非易失性成员函数是读取的道德等价物(对于用户定义的类型),那么为什么不允许编译器消除对这种函数的重复调用?例如

class C {
...
public:
    int get() const;
}

int main() {
    C c;
    int x{c.get()};
    x = c.get(); // why not allow the compiler to eliminate this call
}

允许这一点的论据与copy elision的参数相同:虽然它改变了操作语义,但它应该适用于遵循良好语义实践的代码,并且在效率/模块性方面提供了实质性的改进。 (在这个例子中,它显然是愚蠢的,但它在内联函数时消除冗余的迭代安全检查变得非常有价值。)

当然,对于返回非const引用的函数,只允许返回值或const引用的函数,这是没有意义的。

我的问题是,是否有一个基本的技术论据反对这一点并不同样适用于复制省略。

注意:为了清楚起见,我不建议编译器查看get()的定义。我说get()的声明本身应该允许编译器忽略额外的调用。我没有声称它保留了as-if规则;我声称,就像在复制省略中一样,我们希望允许编译器违反as-if规则。如果您要编写代码,希望副作用在语义上可见,并且不希望消除冗余调用,则不应将方法声明为const。

3 个答案:

答案 0 :(得分:4)

基于对问题的澄清的新答案

C::get需要比const更强的注释。就目前而言,const是一种承诺,即该方法不会(在概念上)修改对象。它不能保证与全局状态或副作用的相互作用。

因此,如果C ++标准的新版本为as-if规则划分了另一个例外,就像复制省略那样,仅基于方法被标记为const的事实,它会破坏很多现有的代码。标准委员会似乎很难不破坏现有的代码。

(复制elision可能也破坏了一些代码,但我认为与你提议的相比,它实际上是一个非常狭窄的例外。)

您可能会争辩说我们应该重新指定const对方法声明的含义,赋予它更强的含义。这意味着你不能再使用C::print方法作为const,所以看起来这种方法也会破坏很多现有的代码。

所以我们必须发明一个新的注释,比如pure_function。为了将其纳入标准,您必须提出它并且可能说服至少一个编译器制造商将其作为扩展来实现,以说明它是可行和有用的。

我怀疑增量效用非常低。如果你的C::get是微不足道的(没有与全局状态的交互并且没有可观察到的副作用),那么你也可以在类定义中定义它,从而使其可用于内联。我相信内联将允许编译器生成与声明上的pure_function标签一样最优的代码(甚至可能更多),因此我不希望pure_function标签的增量优势足以说服标准委员会,编译器制造商和语言用户采用它。

原始回答

C::get可能依赖于全局状态,并且它可能具有可观察到的副作用,其中任何一个都会使得忽略第二次呼叫成为错误。它会违反as-if规则。

问题是编译器在呼叫站点进行优化时是否知道这一点。在编写示例时,只有C::get的声明在范围内。该定义在其他地方,可能是在另一个编译单元中。因此编译器在编译和优化调用代码时必须假设最差。

现在,如果C::get的定义既微不足道又在视野中,那么我认为理论上编译器可以认识到没有副作用或非确定性行为,但我怀疑最多优化者变得好斗。除非C::get被内联,否则我认为分析路径会呈指数级增长。

如果你想跳过整个赋值语句(而不是第二次调用C::get),那么编译器还必须检查赋值运算符的副作用和依赖于全局状态的顺序确保优化不会违反as-if规则。

答案 1 :(得分:0)

首先const - 方法(或引用)与优化器完全无关,因为constness可以合法地转换(使用const-cast),因为在引用的情况下,可以别名。 Const正确性旨在帮助程序员,而不是优化器(另一个问题是它是否真的有帮助,但这是一个单独的无关讨论)。

此外,为了避免调用函数,优化器还需要确保结果不依赖并且不会影响全局状态。

编译器有时会有一种方法来声明一个函数是纯粹的",即结果只取决于参数并且不会影响全局状态(如sin(x)),但是你如何声明它们是依赖于实现的,因为C ++标准并没有涵盖这个语义概念。

另请注意const中的const reference一词描述了引用的属性,而不是引用对象的属性。没有人知道对象的常数,你给出了一个const引用,当你的参考仍然在你手中时,对象确实可以改变甚至不存在。 const引用意味着您不能使用该引用更改对象,而不是对象是常量或者它将在一段时间内保持不变。

有关为什么const引用和值是两个非常不同的语义概念以及如果您混淆它们可以遇到的微妙错误的原因的说明,请参阅此more detailed answer

答案 2 :(得分:0)

Adrian McCarthy对你问题的第一个答案就是尽可能清楚:

成员函数的const - 是一个承诺,即不会修改外部可见状态(例如,在对象实例中标记mutable个变量)。

你会期望一个只报告对象内部状态的const成员函数总是返回相同的答案。然而,它也可以与不断变化的现实世界互动,并且每次都会返回不同的答案。

如果是返回当前时间的功能怎么办?

让我们把它作为一个例子。

这是一个将时间戳(double)转换为人类可读字符串的类。

class time_str {
    // time and its format
    double time;
    string time_format;
public:
    void set_format(const string& time_format);
    void set_time(double time);
    string get_time() const;
    string get_current_time() const;
};

它是如此使用(笨拙):

time_str a;
a.set_format("hh:mm:ss");
a.set_time(89.432);
cout << a.get_time() << endl;

到目前为止一切顺利。每次调用a.get_time();都会返回相同的结果。

然而,在某些时候,我们决定引入一个便利功能,它以相同的格式返回当前时间:

cout << a.get_time() << " is different from " << a.get_current_time() << endl;

它是const,因为它不以任何方式改变对象的状态(尽管它访问时间格式)。但是,显然每次调用get_current_time() 都必须返回不同的答案。