复制elision用于传值参数

Mus*_*ful 17 c++ pass-by-value copy-elision

特定

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;
};
Run Code Online (Sandbox Code Playgroud)

假设我们跑了Box box(Range(0.0,1.0),Range(0.0,2.0)).

启用优化的现代编译器是否可以避免Range在此构造期间完全复制对象?(即构建Range内部的对象box以开始?)

int*_*jay 24

实际上,在Range传递给构造函数的每个对象上执行了两个副本.第一种情况发生在将临时Range对象复制到函数参数中时.根据101010的回答中给出的参考,可以省略这一点.在某些特定情况下可以执行复制省略.

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

当复制构造函数具有副作用(例如YSC答案中的打印件)时,仍可以对第一个副本执行复制省略,但必须保留第二个副本.

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

您可以通过分析生成的程序集来看到这一点.在此示例中,编译器不仅优化副本,还优化Box对象本身的构造:

Box box(Range(a,b),Range(c,d));
std::cout << box.x.from;
Run Code Online (Sandbox Code Playgroud)

生成相同的程序集:

std::cout << a;
Run Code Online (Sandbox Code Playgroud)

  • 如果您更清楚地引用这里有两件事,也许这可以得到改进 (1) 复制省略,无论复制构造函数/析构函数如何实现,它都允许删除副本,是 C++ 特定规则和 (2) 作为If 规则允许删除任何不改变行为的代码,是一个通用的优化器规则。其他答案在 (2) 处失败,因为他们开始检测复制构造函数/析构函数调用以见证副本,这反过来又禁止了“As If”规则,因此优化器保留了复制行为。 (2认同)
  • @JordanMelo 如果你问为什么第一个副本可以被省略,那是因为标准允许它在 [copy elision](http://en.cppreference.com/w/cpp/language/copy_elision) 的规则中即使构造函数有副作用也适用。这里的相关规则是从临时文件中删除副本。如果您问为什么必须保留第二个,那是因为它不属于允许的复制省略应用程序,并且由于它具有副作用,[as-if 规则](http ://en.cppreference.com/w/cpp/language/as_if)。 (2认同)
  • @JordanMelo正确.构造函数参数`from`和`to`是命名变量,因此不是临时变量. (2认同)

YSC*_*YSC 5

它应该,但我没有让它工作(现场示例)。编译器可能会检测到构造函数的副作用并决定不使用复制省略。

#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;
}
Run Code Online (Sandbox Code Playgroud)

编译运行:

clang++ -std=c++14 -O3 -Wall -Wextra -pedantic -Werror -pthread main.cpp && ./a.out
Run Code Online (Sandbox Code Playgroud)

输出:

Range(double,double)
Range(double,double)
Range(const Range&)
Range(const Range&)
Box(Range,Range)
1
Run Code Online (Sandbox Code Playgroud)

  • 但是您不是从复制构造函数打印。 (2认同)
  • 这确实省略了进入按值构造函数的两个复制构造函数,它只是没有将复制从参数中删除到 `x` 和 `y` 成员中。一个常见的模式是将按值然后 `std::move` 放入数据成员中。 (2认同)
  • @YSC 复制省略不关心副作用。这就是重点。 (2认同)

101*_*010 5

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

(31.3) -- 当尚未绑定到引用 (12.2) 的临时类对象被复制/移动到具有相同类型的类对象时(忽略 cv 限定),可以通过以下方式省略复制/移动操作将临时对象直接构造到省略的复制/移动的目标中。

任何下降编译器都在这个特定的上下文中应用复制省略。然而,在OP示例中,发生了两个副本。

  1. 构造函数中传递的临时对象(可以根据上面提到的标准进行删除)。
  2. 构造函数的初始值设定项列表中的副本Box(无法删除)。

您可以在这个演示中看到它,其中复制构造函数仅被调用两次。

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

  • 我无法让它发生。举一个例子对说服我有很大帮助。 (3认同)