复制按值传递参数的省略

Copy elision for pass-by-value arguments

给出

struct Range{
    Range(double from, double to) : from(from), to(to) {}
    double from;
    double to;
};

struct Box{
    Box(Range x, Range y) : x(x), y(y) {}
    Range x;
    Range y;
};

假设我们 运行 Box box(Range(0.0,1.0),Range(0.0,2.0)).

启用优化的现代编译器能否避免在此构造过程中完全复制 Range 对象? (即首先在 box 中构建 Range 对象?)

应该,但是 I fail to make it work (live example)。编译器可能会检测到构造函数的副作用并决定不使用复制省略。

#include <iostream>

struct Range{
    Range(double from, double to) : from(from), to(to) { std::cout << "Range(double,double)" << std::endl; }
    Range(const Range& other) : from(other.from), to(other.to) { std::cout << "Range(const Range&)" << std::endl; }
    double from;
    double to;
};

struct Box{
    Box(Range x, Range y) : x(x), y(y) { std::cout << "Box(Range,Range)" << std::endl; }
    Box(const Box& other) : x(other.x), y(other.y) { std::cout << "Box(const Box&)" << std::endl; }
    Range x;
    Range y;
};


int main(int argc, char** argv)
{
    (void) argv;
    const Box box(Range(argc, 1.0), Range(0.0, 2.0));
    std::cout << box.x.from << std::endl;
    return 0;
}

编译 & 运行:

clang++ -std=c++14 -O3 -Wall -Wextra -pedantic -Werror -pthread main.cpp && ./a.out

输出:

Range(double,double)
Range(double,double)
Range(const Range&)
Range(const Range&)
Box(Range,Range)
1

是的,特别是这种复制省略上下文属于 12.8/p31.3 复制和移动 class 对象 [class.copy 中指定的复制省略标准] 标准:

(31.3) -- 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 type (ignoring cv-qualification), the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move.

任何后代编译器都会在此特定上下文中应用复制省略。但是,在 OP 示例中发生了两个副本。

  1. 在构造函数中传递的临时对象(可以按照上述标准省略)。
  2. Box 构造函数的初始化列表中的副本(不能省略)。

您可以在这个 demo 中看到它,其中仅调用了 2 次复制构造函数。

还请记住,因为标准允许在特定上下文中进行复制省略优化,并不意味着编译器供应商有义务这样做。复制省略是唯一允许的可以改变可观察到的副作用的优化形式。因此,由于某些编译器不会在所有允许的情况下(例如,在调试模式下)执行复制省略,依赖于copy/move构造函数的副作用的程序和析构函数不可移植。

它可以的事实并不意味着它一定会。在这个 Demo 中看到它,很明显您正在创建两个副本。提示,输出包含两次:

copy made

copy made

实际上对传递给构造函数的每个 Range 对象执行了两个副本。第一个发生在将临时 Range 对象复制到函数参数时。这可以根据 101010 的回答中给出的参考省略。可以进行复制省略的specific circumstances

第二次复制发生在将函数参数复制到成员(如构造函数初始化列表中指定)时。这不能省略,这就是为什么您仍然看到在 YSC 的回答中为每个参数制作了一个副本。

当拷贝构造函数有副作用时(比如YSC答案中的打印),第一个拷贝仍然可以进行拷贝省略,但是第二个拷贝必须保留。

但是,如果编译器不改变观察到的程序行为(这称为 "as-if" 规则),则编译器始终可以自由进行更改。这意味着如果复制构造函数没有副作用并且删除构造函数调用不会改变结果,编译器甚至可以自由删除第二个副本。

您可以通过分析生成的程序集看到这一点。在 this example 中,编译器不仅优化了副本,甚至优化了 Box 对象本身的构造:

Box box(Range(a,b),Range(c,d));
std::cout << box.x.from;

生成与以下相同的程序集:

std::cout << a;