关于c ++:什么是复制省略和返回值优化?

What are copy elision and return value optimization?

什么是删除副本?什么是(命名的)返回值优化?它们意味着什么?

它们会在什么情况下发生?什么是限制?

  • 如果你被提到这个问题,你可能正在寻找引言。
  • 有关技术概述,请参见标准参考。
  • 见常见案例。


介绍

有关技术概述,请跳到此答案。

对于出现复制删除的常见情况-跳到此答案。

复制省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的(潜在的昂贵)复制。它使按值返回或传递值在实践中可行(适用限制条件)。

它是唯一的优化形式,Elides(ha!)即使复制/移动对象有副作用,也可以应用"假设规则-复制"省略。

以下例子摘自维基百科:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct C {
  C() {}
  C(const C&) { std::cout <<"A copy was made.
"
; }
};

C f() {
  return C();
}

int main() {
  std::cout <<"Hello World!
"
;
  C obj = f();
}

根据编译器设置,以下输出均有效:

Hello World!
A copy was made.
A copy was made.

Hello World!
A copy was made.

Hello World!

这也意味着可以创建的对象更少,因此也不能依赖于调用的特定数量的析构函数。在复制/移动构造函数或析构函数中不应该有关键的逻辑,因为您不能依赖于它们被调用。

如果省略了对复制或移动构造函数的调用,则该构造函数必须仍然存在,并且必须可访问。这样可以确保复制省略不允许复制通常不可复制的对象,例如,因为它们具有私有或已删除的复制/移动构造函数。

C++ 17:在C++ 17中,当直接返回对象时保证复制删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct C {
  C() {}
  C(const C&) { std::cout <<"A copy was made.
"
; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout <<"Hello World!
"
;
  C obj = f(); //Copy constructor isn't called
}


标准参考

对于技术性较低的观点和介绍,请跳到此答案。

对于出现复制删除的常见情况-跳到此答案。

标准中定义了删除副本的方法:

12.8复制和移动类对象[类.复制]

作为

31) When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases,
the implementation treats the source and target of the omitted copy/move operation as simply two different
ways of referring to the same object, and the destruction of that object occurs at the later of the times
when the two objects would have been destroyed without the optimization.123 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 cvunqualified
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

— in a throw-expression, when the operand is the name of a non-volatile automatic object (other than a
function or catch-clause parameter) whose scope does not extend beyond the end of the innermost
enclosing try-block (if there is one), the copy/move operation from the operand to the exception
object (15.1) can be omitted by constructing the automatic object directly into the exception object

— 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

— when the exception-declaration of an exception handler (Clause 15) declares an object of the same type
(except for cv-qualification) as the exception object (15.1), the copy/move operation can be omitted
by treating the exception-declaration as an alias for the exception object if the meaning of the program
will be unchanged except for the execution of constructors and destructors for the object declared by
the exception-declaration.

123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one
object destroyed for each one constructed.

示例如下:

1
2
3
4
5
6
7
8
9
10
11
class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

并解释说:

Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing:
the copying of the local automatic object t into the temporary object for the return value of function f()
and the copying of that temporary object into object t2. Effectively, the construction of the local object t
can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program
exit. Adding a move constructor to Thing has the same effect, but it is the move construction from the
temporary object to t2 that is elided.


常见的省略形式

有关技术概述,请跳到此答案。

对于技术性较低的观点和介绍,请跳到此答案。

(命名)返回值优化是一种常见的复制省略形式。它指的是一个从方法返回值的对象的副本被删除的情况。标准中的示例说明了命名的返回值优化,因为对象是命名的。

1
2
3
4
5
6
7
8
9
10
11
class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

当返回临时值时,将进行常规返回值优化:

1
2
3
4
5
6
7
8
9
10
class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

其他发生副本删除的常见地方是当临时值通过时:

1
2
3
4
5
6
7
8
9
class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

或者当异常被抛出并被值捕获时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }            
}

删除副本的常见限制是:

  • 多个返回点
  • 条件初始化

大多数商业级编译器支持copy elision&;(n)rvo(取决于优化设置)。


复制省略是一种编译器优化技术,可以消除不必要的对象复制/移动。

在下列情况下,编译器可以省略复制/移动操作,因此不调用关联的构造函数:

  • nrvo(已命名的返回值优化):如果函数按值返回类类型,并且返回语句的表达式是具有自动存储持续时间(不是函数参数)的非易失性对象的名称,则可以省略非优化编译器执行的复制/移动。如果是这样,则返回值直接构建在存储中,否则函数的返回值将被移动或复制到存储中。
  • rvo(返回值优化):如果函数返回一个无名称的临时对象,该对象将被幼稚的编译器移动或复制到目标中,则可以按1省略复制或移动。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    #include <iostream>  
    using namespace std;

    class ABC  
    {  
    public:  
        const char *a;  
        ABC()  
         { cout<<"Constructor"<<endl; }  
        ABC(const char *ptr)  
         { cout<<"Constructor"<<endl; }  
        ABC(ABC  &obj)  
         { cout<<"copy constructor"<<endl;}  
        ABC(ABC&& obj)  
        { cout<<"Move constructor"<<endl; }  
        ~ABC()  
        { cout<<"Destructor"<<endl; }  
    };

    ABC fun123()  
    { ABC obj; return obj; }  

    ABC xyz123()  
    {  return ABC(); }  

    int main()  
    {  
        ABC abc;  
        ABC obj1(fun123());//NRVO  
        ABC obj2(xyz123());//NRVO  
        ABC xyz ="Stack Overflow";//RVO  
        return 0;  
    }

    **Output without -fno-elide-constructors**  
    root@ajay-PC:/home/ajay/c++# ./a.out  
    Constructor    
    Constructor  
    Constructor  
    Constructor  
    Destructor  
    Destructor  
    Destructor  
    Destructor  

    **Output with -fno-elide-constructors**  
    root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
    root@ajay-PC:/home/ajay/c++# ./a.out  
    Constructor  
    Constructor  
    Move constructor  
    Destructor  
    Move constructor  
    Destructor  
    Constructor  
    Move constructor  
    Destructor  
    Move constructor  
    Destructor  
    Constructor  
    Move constructor  
    Destructor  
    Destructor  
    Destructor  
    Destructor  
    Destructor

    即使发生了复制省略,并且没有调用复制/移动构造函数,它也必须存在并可访问(就好像根本没有进行优化一样),否则程序是格式错误的。

    您应该只在不会影响软件的可观察行为的地方允许这样的副本删除。拷贝删除是唯一允许有(即删除)可观察到的副作用的优化形式。例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include <iostream>    
    int n = 0;    
    class ABC    
    {  public:  
     ABC(int) {}    
     ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
    };                     // it modifies an object with static storage duration    

    int main()  
    {  
      ABC c1(21); // direct-initialization, calls C::C(42)  
      ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

      std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
      return 0;  
    }

    Output without -fno-elide-constructors  
    root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
    root@ajay-PC:/home/ayadav# ./a.out  
    0

    Output with -fno-elide-constructors  
    root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
    root@ajay-PC:/home/ayadav# ./a.out  
    1

    gcc提供了-fno-elide-constructors选项来禁用复制删除。如果要避免可能的副本删除,请使用-fno-elide-constructors

    现在,几乎所有编译器都在启用优化(如果没有其他选项设置为禁用优化)时提供副本省略。

    结论

    每删除一个拷贝,就省略了拷贝的一个构造和一个匹配的销毁,从而节省了CPU时间,并且没有创建一个对象,从而节省了堆栈帧上的空间。