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

Does c++ compiler optimize a = b + c

谁给我投了反对票,介意解释一下原因吗?我认为这是一个合理的问题,所有答案都非常有帮助。

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

当 returning 一个对象并调用赋值运算符时,我似乎会复制两次东西。这是在编译器中优化的吗?如果是,如何?如果涉及铸造,这似乎更棘手。

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

[编辑:] 哇,我没想到在 return-by-value 中使用 const 会受到批评。我认为在 return-by-value 非内置类型时使用 const 是令人鼓舞的吗?我记得在哪里见过它。

复制初始化不使用赋值运算符,它使用复制或移动构造函数。由于您的运算符愚蠢地 return 是一个 const 对象,移动是不可能的,因此它将使用复制构造函数。

但是,从临时对象初始化对象是允许复制省略的情况之一,因此任何体面的编译器都应该这样做,直接将a初始化为return 值而不是创建临时值。

大多数编译器会使用copy-elision来优化它。通过调用 MyClass::operator+ 创建的临时对象将直接构造到 a 而不是调用复制构造函数。

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

查看 here 了解有关复制省略的更多信息。

§12.8/31 中的标准描述了一种称为复制省略的优化:

This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv- unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

因此在行

MyClass a = b + c;

operator+返回的临时对象直接构造成a,没有出现不必要的copies/moves,甚至在return operator+ 中的语句。演示:

struct MyClass
{
    int i;

    MyClass operator+( MyClass const& m ) {
        MyClass r = m.i + i;
        return r;
    }

    MyClass(int i) : i(i) {std::cout << "Ctor!\n";}
    // Move ctor not implicitly declared, see §12.8/9
    MyClass(MyClass const&) {std::cout << "Copy-Ctor!\n";}
    ~MyClass() {std::cout << "* Dtor!\n";}
};

int main() {
    MyClass c{7}, b{3},
            a = b + c;
}

任何像样的编译器的输出:

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

Live on Coliru

为了更直接地了解您可以期待什么,让我们从一个简单的 class 开始:

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++ 我们得到这个:

[ 正常 "stuff" 设置进入 main 已删除 ]

; 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 对象并使用构造和 return 的第三个 Integer 对象的重载运算符添加了它们,编译器还是识破了我们所有的诡计,并且 "realized"我们正在做的只是使用 atoi 读取几个 int,将它们加在一起,然后打印出我们得到的 int 结果。

看到之后,它完全消除了函数调用并且不调用或 return 任何东西——它只是读取两个整数,将它们相加,然后打印出结果。

使用 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 class 的所有痕迹都消失了。

让我们将其与根本不使用 class 的结果进行比较:

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<<

除了显示原始文件行号的注释外,代码与我们使用 class.

得到的代码完全相同

我不会浪费 space 来复制粘贴 g++ 的结果;无论我们是否使用 class 和重载运算符进行加法运算,它都会生成相同的代码。