c ++编译器是否优化a = b + c

时间:2015-04-10 13:36:34

标签: c++ operator-overloading compiler-optimization

whover downvoted me,介意解释原因?我认为这是一个合理的问题,所有答案都非常有用。

理论上,当我MyClass a = b + c时,它应该首先调用const MyClass operator+,返回一个const MyClass对象,然后调用赋值运算符来创建对象a。

似乎我会在返回一个对象并调用赋值运算符时复制两次。这是在编译器中优化的吗?如果有,怎么样?如果它涉及铸造似乎更棘手。

让我们假设我们正在谈论g ++,这几乎是c ++编译器的黄金标准。 [编辑:好的,让我们说最常用的]

[编辑:]哇,我没有期望使用const按值返回会受到批评。我认为在非内置类型的返回值时使用const是值得的吗?我记得在某个地方看过它。

4 个答案:

答案 0 :(得分:2)

复制初始化不使用赋值运算符,它使用复制或移动构造函数。由于您的运算符愚蠢地返回const对象,因此无法移动,因此它将使用复制构造函数。

但是,从临时初始化对象是允许 copy elision 的情况之一,因此任何体面的编译器都应该这样做,直接初始化a作为返回值而不是创造一个临时的。

答案 1 :(得分:1)

大多数编译器都会使用 copy-elision 对其进行优化。从调用MyClass::operator+创建的临时文件将直接构造到a而不是调用复制构造函数。

另请注意,MyClass a = ...不会调用赋值运算符,而是调用复制构造函数。这称为复制初始化。

有关复制省略的更多信息,请查看here

答案 2 :(得分:0)

有一项名为copy elision的优化,由§12.8/ 31中的标准描述:

  

复制/移动操作的省略,称为 copy elision ,是   在下列情况下允许(可以合并到   消除多份副本):

     
      
  • 在具有类返回类型的函数中的return语句中,当表达式是a的名称时   非易失性自动对象(函数或catch子句参数除外)具有相同的cv-   非限定类型作为函数返回类型,可以通过构造省略复制/移动操作   自动对象直接进入函数的返回值

  •   
  • 当一个尚未绑定到引用(12.2)的临时类对象被复制/移动到具有相同的类对象时   cv-unqualified type,可以省略复制/移动操作   将临时对象直接构造到目标中   省略复制/移动

  •   

因此在行

MyClass a = b + c;

operator+返回的临时对象直接构建到a ,并且不会发生不必要的副本/移动,即使在return语句中也没有{1}}。演示:

operator+

任何体面编译器的输出:

  

Ctor!
Ctor!
Ctor!
  * Dtor!
  * Dtor!
  * Dtor!

Live on Coliru

答案 3 :(得分:0)

为了更直接地了解您的期望,让我们从这样一个简单的类开始:

class Integer {
    int a;
    public:
    Integer(int a) : a(a) {}

    friend Integer operator+(Integer a, Integer b) {
        return Integer(a.a + b.a);
    }

    friend std::ostream &operator<<(std::ostream &os, Integer const &i) {
        return os << i.a;
    }
};

为了演示,我们添加一个main来从外部世界读取一些数据,创建几个Integer对象,然后打印出添加它们的结果。输入和输出将来自外部世界,因此编译器无法提取过于的花样,并优化所有内容。

int main(int argc, char **argv) {
    Integer a(atoi(argv[1])), b(atoi(argv[2]));
    Integer c = a + b;    // Line 20
    std::cout << c;
}

注意那里的line 20 - 它在下面变得很重要。

现在,让我们编译它,看看编译器生成了什么代码。使用VC ++我们得到这个:

[普通&#34;东西&#34;设置main elided]的条目

; Line 19
    mov rcx, QWORD PTR [rdx+8]
    mov rdi, rdx
    call    atoi
    mov rcx, QWORD PTR [rdi+16]
    mov ebx, eax
    call    atoi
; Line 20
    lea edx, DWORD PTR [rax+rbx]
; Line 21
    call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<

所以即使我们已经创建了两个Integer对象并使用构造并返回第三个Integer对象的重载运算符添加它们,编译器已经看透了我们所有的子程序,并且&#34;实现了&#34;我们正在做的只是使用int阅读几个atoi,将它们添加到一起,然后打印出我们得到的int

看过它之后,它完全消除了函数调用,并且没有调用或返回任何东西 - 它只读取两个整数,将它们相加,并打印出结果。

使用gcc的结果几乎相同:

movq    8(%rbx), %rcx
call    atoi                    ; <--- get first item
movq    16(%rbx), %rcx
movl    %eax, %esi
call    atoi                    ; <--- get second item
movq    .refptr._ZSt4cout(%rip), %rcx
leal    (%rsi,%rax), %edx       ; <--- the addition
call    _ZNSolsEi               ; <--- print result

它稍微重新排列了代码,但是终极几乎完全相同 - 我们Integer类的所有痕迹都消失了。

让我们将其与我们在没有使用课程的情况下进行比较:

int main(int argc, char **argv) {
    int a = atoi(argv[1]);
    int b = atoi(argv[2]);
    int c = a + b;
    std::cout << c;
}

使用VC ++,会产生以下结果:

; Line 5
    mov rcx, QWORD PTR [rdx+8]
    mov rdi, rdx
    call    atoi
; Line 6
    mov rcx, QWORD PTR [rdi+16]
    mov ebx, eax
    call    atoi
; Line 7
    lea edx, DWORD PTR [rbx+rax]
; Line 8
    call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<

除了显示原始文件中行号的注释外,代码与我们使用该类的内容完全相同

我不会浪费空间来复制和粘贴g ++的结果;它也产生相同的代码,无论我们是否使用我们的类和重载运算符来进行添加。