为什么这个unique_ptr的死店不能被淘汰?
Why this dead store of unique_ptr cannot be eliminated?
#include <memory>
#include <vector>
using namespace std;
vector<unique_ptr<int>> e;
void f(unique_ptr<int> u) {
e.emplace_back(move(u));
}
对于 Clang and GCC,上面的代码片段生成如下内容:
f(std::unique_ptr<int, std::default_delete<int> >):
mov rsi, QWORD PTR e[rip+8] # rsi: vector.end_ptr
cmp rsi, QWORD PTR e[rip+16] # [e + rip + 16]: vector.storage_end_ptr
je .L52 # Slow path, need to reallocate
mov rax, QWORD PTR [rdi] # rax: unique_ptr<int> u
add rsi, 8 # end_ptr += 8
mov QWORD PTR [rdi], 0 # <==== Do we need to set the argument u to null here?
mov QWORD PTR [rsi-8], rax # *(end_ptr - 8) = u
mov QWORD PTR e[rip+8], rsi # update end_ptr
ret
.L52: # omitted
我想知道为什么编译器会在这个函数中生成mov QWORD PTR[rdi], 0
?是否有任何约定要求编译器这样做?
此外,对于像 this 这样更简单的情况:
void f(unique_ptr<int> u);
void h(int x) {
auto p = make_unique<int>(x);
f(move(p));
}
为什么编译器会生成:
call operator delete(void*, unsigned long)
在 h()
的末尾,假设在调用 f
?
之后 p 总是 nullptr
在这两种情况下,答案都是:因为你移动的对象仍然会被销毁。
如果您查看为调用
生成的代码
void f(unique_ptr<int> u);
您会注意到调用者为参数 u
创建对象,然后按照调用约定的要求调用其析构函数。如果对 f()
的调用是内联的,编译器很可能能够优化它。但是为 f()
生成的代码无法控制 u
的析构函数,因此必须将 u
的内部指针设置为零,假设 u
的析构函数将 运行 在函数 returns.
之后
在你的第二个例子中,我们有一种相反的情况:
void h(int x) {
auto p = make_unique<int>(x);
f(move(p));
}
与名称可能暗示的相反,std::move()
实际上并不移动对象。它所做的只是转换为一个右值引用,该引用允许该引用的接收者从所引用的对象中移动——如果他愿意的话。实际移动只会发生,例如,当另一个对象通过移动构造函数从给定参数构造时。由于编译器不知道在 h()
的定义点 f()
内部发生了什么,它不能假设 f()
总是从给定的对象移动。例如,f()
可以简单地 return 或仅在某些情况下移动而在其他情况下不移动。因此,编译器必须假定该函数可能 return 而不从对象移动,并且必须为析构函数发出 delete
。该函数还可以执行移动分配而不是移动构造,在这种情况下,仍然需要外部析构函数来释放之前由新对象分配的所有权持有的对象的所有权……
#include <memory>
#include <vector>
using namespace std;
vector<unique_ptr<int>> e;
void f(unique_ptr<int> u) {
e.emplace_back(move(u));
}
对于 Clang and GCC,上面的代码片段生成如下内容:
f(std::unique_ptr<int, std::default_delete<int> >):
mov rsi, QWORD PTR e[rip+8] # rsi: vector.end_ptr
cmp rsi, QWORD PTR e[rip+16] # [e + rip + 16]: vector.storage_end_ptr
je .L52 # Slow path, need to reallocate
mov rax, QWORD PTR [rdi] # rax: unique_ptr<int> u
add rsi, 8 # end_ptr += 8
mov QWORD PTR [rdi], 0 # <==== Do we need to set the argument u to null here?
mov QWORD PTR [rsi-8], rax # *(end_ptr - 8) = u
mov QWORD PTR e[rip+8], rsi # update end_ptr
ret
.L52: # omitted
我想知道为什么编译器会在这个函数中生成mov QWORD PTR[rdi], 0
?是否有任何约定要求编译器这样做?
此外,对于像 this 这样更简单的情况:
void f(unique_ptr<int> u);
void h(int x) {
auto p = make_unique<int>(x);
f(move(p));
}
为什么编译器会生成:
call operator delete(void*, unsigned long)
在 h()
的末尾,假设在调用 f
?
nullptr
在这两种情况下,答案都是:因为你移动的对象仍然会被销毁。
如果您查看为调用
生成的代码void f(unique_ptr<int> u);
您会注意到调用者为参数 u
创建对象,然后按照调用约定的要求调用其析构函数。如果对 f()
的调用是内联的,编译器很可能能够优化它。但是为 f()
生成的代码无法控制 u
的析构函数,因此必须将 u
的内部指针设置为零,假设 u
的析构函数将 运行 在函数 returns.
在你的第二个例子中,我们有一种相反的情况:
void h(int x) {
auto p = make_unique<int>(x);
f(move(p));
}
与名称可能暗示的相反,std::move()
实际上并不移动对象。它所做的只是转换为一个右值引用,该引用允许该引用的接收者从所引用的对象中移动——如果他愿意的话。实际移动只会发生,例如,当另一个对象通过移动构造函数从给定参数构造时。由于编译器不知道在 h()
的定义点 f()
内部发生了什么,它不能假设 f()
总是从给定的对象移动。例如,f()
可以简单地 return 或仅在某些情况下移动而在其他情况下不移动。因此,编译器必须假定该函数可能 return 而不从对象移动,并且必须为析构函数发出 delete
。该函数还可以执行移动分配而不是移动构造,在这种情况下,仍然需要外部析构函数来释放之前由新对象分配的所有权持有的对象的所有权……