In C++14中,当关闭RVO/NRVO时,对象是如何返回的?

huangapple go评论54阅读模式
英文:

When in C++14 with RVO/NRVO closed, how is the object returned?

问题

以下是程序输出的翻译:

我正在学习关于移动语义,所以我编写了以下的小程序来进行练习:

// 代码省略

我通过编译器选项 `-fno-elide-constructors` 关闭了 clang 的 RVO/NRVO,并使用了 `-std=c++14`。我得到了以下输出,非常令人困惑:

// 输出省略

到底发生了什么?为什么会调用许多意外的移动构造和销毁操作?我尝试解释了一些输出,但仍然有许多输出我不理解。你能帮我吗?非常感谢。
英文:

I am learning about move semantics, so I wrote a small program as follows to practice:

#include <iostream>

using namespace std;

int one_int = 123;

class A {
public:
	int *a;

	A(int *ptr) : a(ptr) {
        cout << "In A's constructor..." << endl;
    }

	A(const A &other) {
        cout << "In A's copy constructor..." << endl;
        a = other.a;
    }

	A(A &&other) noexcept {
        cout << "In A's move constructor..." << endl;
        a = other.a;
        other.a = nullptr;
    }

	~A() {
        cout << "In A's destructor..." << endl;
    }
};

A make_obj() {
    cout << "In make_obj..." << endl;
    return A(&one_int);
}

A make_obj_by_move(A &&source) {
    cout << "In make_obj_by_move..." << endl;
    return A(static_cast<A&&>(source));
}

A make_obj_by_copy(A source) {
    cout << "In make_obj_by_copy..." << endl;
    return A(source);
}

int main() {
    A obj2 = make_obj_by_move(make_obj());
    cout << endl;
    A obj1 = make_obj_by_copy(make_obj());
    cout << endl;
    // to ensure that the information printed when obj1 and obj2 are destroyed is separated from the above ones
    obj1.a = nullptr;
    obj2.a = nullptr;
    return 0;
}

I turned off RVO/NRVO of clang by the -fno-elide-constructors compiler option and use -std=c++14. I got the following output, very confusing:

In make_obj...                                
In A's constructor...                   // make tmp obj
In A's move constructor...              // w/o RVO, need another construction to get return value?
In A's destructor...                    // destory the temp obj in make_obj scope?                
In make_obj_by_move...                
In A's move constructor...              // call the move constructor like expected? I guess by the position of "copy constructor" below
In A's move constructor...              // what happened here ???
In A's destructor...                    // destory which?            
In A's move constructor...              // w/o NRVO, need another construction to get obj1?
In A's destructor...                    // destory which?
In A's destructor...                    // destory which?
In make_obj...                            
In A's constructor...                   // make tmp obj
In A's move constructor...              // w/o RVO, need another construction to get return value?
In A's destructor...                    // destory the temp obj in make_obj scope?            
In A's move constructor...              // what happened here ???
In make_obj_by_copy...                
In A's copy constructor...              // call the copy constructor like expected
In A's move constructor...              // what happened here ???
In A's destructor...                    // destory which?            
In A's move constructor...              // w/o NRVO, need another construction to get obj2?
In A's destructor...                    // destory which?
In A's destructor...                    // destory which?
In A's destructor...                    // destory which?            
In A's destructor...                    // destory obj1/obj2
In A's destructor...                    // destory obj2/obj1

What exactly happened here? Why have many unexpected move constructions and destructions been called? I tried to explain some of the output, but there are still many lines of output that I don't understand. Can you help me? Thank you very much.

答案1

得分: 1

在没有任何拷贝省略的情况下,通常会涉及返回函数的三个对象:

  1. 你传递给 return 关键字的对象。
  2. 函数的返回值。
  3. 由返回值初始化的对象。
  • 你传递给 return 关键字的对象通常是一个函数局部对象,其存储位置位于函数的栈帧中。

  • 函数的返回值存在于函数的作用域之外,在调用函数的代码块中作为临时对象存在,仅存在于函数调用的完整表达式结束之前。

  • 由返回值初始化的对象也存在于调用者的栈帧中,通常具有名称和更广泛的作用域。

拷贝省略允许编译器在正确的情况下将这三个对象合并为一个对象。


我已经注释了你程序的输出,以显示每行所涉及的确切对象:

在 make_obj 中...
在 A 的构造函数中...                     // 创建函数局部临时对象
在 A 的移动构造函数中...                // 通过从函数局部临时对象移动构造 make_obj 的返回值
在 A 的析构函数中...                     // 销毁函数局部临时对象
在 make_obj_by_move 中...
在 A 的移动构造函数中...                // 创建函数局部临时对象
在 A 的移动构造函数中...                // 通过从函数局部临时对象移动构造 make_obj_by_move 的返回值
在 A 的析构函数中...                     // 销毁函数局部临时对象
在 A 的移动构造函数中...                // 通过从 make_obj_by_move 的返回对象移动构造 main 中的 obj1
在 A 的析构函数中...                     // 销毁 make_obj_by_move 的返回对象
在 A 的析构函数中...                     // 销毁 make_obj 的返回对象

在 make_obj 中...
在 A 的构造函数中...                     // 创建函数局部临时对象
在 A 的移动构造函数中...                // 通过从函数局部临时对象移动构造 make_obj 的返回值
在 A 的析构函数中...                     // 销毁函数局部临时对象
在 A 的移动构造函数中...                // 通过从 make_obj 的返回对象移动构造 make_obj_by_copy 的源参数
在 make_obj_by_copy 中...
在 A 的复制构造函数中...                // 通过从源参数复制创建函数局部临时对象
在 A 的移动构造函数中...                // 通过从函数局部临时对象移动构造 make_obj_by_copy 的返回值
在 A 的析构函数中...                     // 销毁函数局部临时对象
在 A 的移动构造函数中...                // 通过从 make_obj_by_move 的返回对象移动构造 main 中的 obj2
在 A 的析构函数中...                     // 销毁 make_obj_by_copy 的返回对象
在 A 的析构函数中...                     // 销毁 make_obj_by_copy 的源参数
在 A 的析构函数中...                     // 销毁 make_obj 的返回对象

在 A 的析构函数中...                     // 销毁 obj2
在 A 的析构函数中...                     // 销毁 obj1

另外一提,在这些情况下,打印每个注释操作中 this 指针的值可能很有用,以便你可以轻松地看到操作所针对的确切对象。

英文:

In the absence of any copy elision, there are usually three objects involved in returning from a function:

  1. The object you pass to the return keyword
  2. The function's return value
  3. The object that gets initialized by the return value
  • The object you give to the return keyword is (often) a function-local object and storage for it lives within the function's stack frame.

  • The function's return value exists outside the function's scope, within the caller's stack frame as a temporary object that exists only until the end of the full-expression in which the function was called.

  • The object that gets initialized by the return value also exists in the caller's stack frame and (usually) has a name and some wider scope within the caller

Copy elision lets the compiler collapse all three of those objects into one in the correct circumstances.


I've annotated your program's output to show exactly which objects each line is talking about:

In make_obj...                                
In A's constructor...         // create function-local temporary object
In A's move constructor...    // construct make_obj's return value by move from the function local temp object
In A's destructor...          // destroy the function-local temp object
In make_obj_by_move...
In A's move constructor...    // create function-local temporary object
In A's move constructor...    // construct make_obj_by_move's return value by move from the function-local temp object
In A's destructor...          // destroy the function-local temp object
In A's move constructor...    // construct obj1 in main by move from make_obj_by_move's return object
In A's destructor...          // destroy make_obj_by_move's return object
In A's destructor...          // destroy make_obj's return object
In make_obj...
In A's constructor...         // create function-local temporary object
In A's move constructor...    // construct make_obj's return value by move from the function local temp object
In A's destructor...          // destroy the function-local temp object
In A's move constructor...    // construct make_obj_by_copy's source parameter by move from make_obj's return object
In make_obj_by_copy...                
In A's copy constructor...    // create function-local temp object by copy from the source parameter
In A's move constructor...    // construct make_obj_by_copy's return value by move from the function-local temp object
In A's destructor...          // destroy the function-local temp object
In A's move constructor...    // construct obj2 in main by move from make_obj_by_move's return object
In A's destructor...          // destroy make_obj_by_copy's return object
In A's destructor...          // destroy make_obj_by_copy's source parameter
In A's destructor...          // destroy make_obj's return object
In A's destructor...          // destroy obj2
In A's destructor...          // destroy obj1

As a side-note, in these sorts of situations it can be useful to print the value of the this pointer in each of your instrumented operations so that you can easily see exactly which object the operation was performed on.

huangapple
  • 本文由 发表于 2023年5月21日 09:28:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/76297942.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定