为什么这个 RAII 只移动类型不能正确地模拟 std::unique_ptr ?
Why doesn't this RAII move-only type properly emulate `std::unique_ptr`?
我从 this question 中获取代码并对其进行编辑,通过显式调用移动构造对象之一的析构函数来产生段错误:
using namespace std;
struct Foo
{
Foo()
{
s = new char[100];
cout << "Constructor called!" << endl;
}
Foo(const Foo& f) = delete;
Foo(Foo&& f) :
s{f.s}
{
cout << "Move ctor called!" << endl;
f.s = nullptr;
}
~Foo()
{
cout << "Destructor called!" << endl;
cout << "s null? " << (s == nullptr) << endl;
delete[] s; // okay if s is NULL
}
char* s;
};
void work(Foo&& f2)
{
cout << "About to create f3..." << endl;
Foo f3(move(f2));
// f3.~Foo();
}
int main()
{
Foo f1;
work(move(f1));
}
编译并运行此代码(使用 G++ 4.9)产生以下输出:
Constructor called!
About to create f3...
Move ctor called!
Destructor called!
s null? 0
Destructor called!
s null? 0
*** glibc detected *** ./a.out: double free or corruption (!prev): 0x0916a060 ***
注意,当析构函数没有显式调用时,不会出现double-free错误。
现在,当我将 s
的类型更改为 unique_ptr<char[]>
并删除 ~Foo()
中的 delete[] s
和 Foo(Foo&&)
中的 f.s = nullptr
(请参阅下面的完整代码),我 没有 得到双重错误:
Constructor called!
About to create f3...
Move ctor called!
Destructor called!
s null? 0
Destructor called!
s null? 1
Destructor called!
s null? 1
这是怎么回事?为什么moved-to对象的数据成员是unique_ptr
时可以显式删除,而在Foo(Foo&&)
中手动处理moved-from对象的失效却不能?由于移动构造函数 是 在创建 f3
时调用的(如 "Move ctor called!" 行所示),为什么第一个析构函数调用(大概是 f3
) 声明 s
是 而不是 null?如果答案只是 f3
和 f2
由于优化而实际上是同一个对象,那么 unique_ptr
做了什么来防止该实现发生相同的问题?
编辑: 根据要求,完整代码使用 unique_ptr
:
using namespace std;
struct Foo
{
Foo() :
s{new char[100]}
{
cout << "Constructor called!" << endl;
}
Foo(const Foo& f) = delete;
Foo(Foo&& f) :
s{move(f.s)}
{
cout << "Move ctor called!" << endl;
}
~Foo()
{
cout << "Destructor called!" << endl;
cout << "s null? " << (s == nullptr) << endl;
}
unique_ptr<char[]> s;
};
void work(Foo&& f2)
{
cout << "About to create f3..." << endl;
Foo f3(move(f2));
f3.~Foo();
}
int main()
{
Foo f1;
work(move(f1));
}
我已经仔细检查过这会产生上面复制的输出。
EDIT2: 实际上,使用 Coliru(参见下面的 T.C.'s link),这个确切的代码 确实 产生双重删除错误。
如果显式调用析构函数,当 f3
超出范围时,它将被隐式调用第二次。这会创建 UB,这就是您的 class 崩溃的原因。
您可以通过在析构函数中将 s
重置为 nullptr
来解决 delete
中的崩溃(因此第二次是 nullptr
)但是 UB在两次调用析构函数时仍然存在。
对于任何具有非平凡析构函数的class,根据核心语言规则将其销毁两次是未定义的行为:
[basic.life]/p1:
The lifetime of an object of type T
ends when:
- if
T
is a class type with a non-trivial destructor (12.4), the destructor call starts, or
- the storage which the object occupies is reused or released.
[class.dtor]/p15:
the behavior is undefined if the destructor is invoked for an object
whose lifetime has ended (3.8)
您的代码销毁了 f3
两次,一次是通过显式析构函数调用,一次是通过离开作用域,因此它具有未定义的行为。
碰巧 libstdc++ 和 libc++ 的 unique_ptr
析构函数都会将空指针分配给存储的指针(libc++ 调用 reset()
;libstdc++ 手动执行)。这不是标准所要求的,并且可以说是一个性能错误,它意味着对原始指针的零开销包装。因此,您的代码 "works" 在 -O0
中。
然而,g++ 在 -O2
能够看到析构函数中的赋值不可能被定义良好的程序观察到,因此它优化了赋值,导致双重删除。
我从 this question 中获取代码并对其进行编辑,通过显式调用移动构造对象之一的析构函数来产生段错误:
using namespace std;
struct Foo
{
Foo()
{
s = new char[100];
cout << "Constructor called!" << endl;
}
Foo(const Foo& f) = delete;
Foo(Foo&& f) :
s{f.s}
{
cout << "Move ctor called!" << endl;
f.s = nullptr;
}
~Foo()
{
cout << "Destructor called!" << endl;
cout << "s null? " << (s == nullptr) << endl;
delete[] s; // okay if s is NULL
}
char* s;
};
void work(Foo&& f2)
{
cout << "About to create f3..." << endl;
Foo f3(move(f2));
// f3.~Foo();
}
int main()
{
Foo f1;
work(move(f1));
}
编译并运行此代码(使用 G++ 4.9)产生以下输出:
Constructor called!
About to create f3...
Move ctor called!
Destructor called!
s null? 0
Destructor called!
s null? 0
*** glibc detected *** ./a.out: double free or corruption (!prev): 0x0916a060 ***
注意,当析构函数没有显式调用时,不会出现double-free错误。
现在,当我将 s
的类型更改为 unique_ptr<char[]>
并删除 ~Foo()
中的 delete[] s
和 Foo(Foo&&)
中的 f.s = nullptr
(请参阅下面的完整代码),我 没有 得到双重错误:
Constructor called!
About to create f3...
Move ctor called!
Destructor called!
s null? 0
Destructor called!
s null? 1
Destructor called!
s null? 1
这是怎么回事?为什么moved-to对象的数据成员是unique_ptr
时可以显式删除,而在Foo(Foo&&)
中手动处理moved-from对象的失效却不能?由于移动构造函数 是 在创建 f3
时调用的(如 "Move ctor called!" 行所示),为什么第一个析构函数调用(大概是 f3
) 声明 s
是 而不是 null?如果答案只是 f3
和 f2
由于优化而实际上是同一个对象,那么 unique_ptr
做了什么来防止该实现发生相同的问题?
编辑: 根据要求,完整代码使用 unique_ptr
:
using namespace std;
struct Foo
{
Foo() :
s{new char[100]}
{
cout << "Constructor called!" << endl;
}
Foo(const Foo& f) = delete;
Foo(Foo&& f) :
s{move(f.s)}
{
cout << "Move ctor called!" << endl;
}
~Foo()
{
cout << "Destructor called!" << endl;
cout << "s null? " << (s == nullptr) << endl;
}
unique_ptr<char[]> s;
};
void work(Foo&& f2)
{
cout << "About to create f3..." << endl;
Foo f3(move(f2));
f3.~Foo();
}
int main()
{
Foo f1;
work(move(f1));
}
我已经仔细检查过这会产生上面复制的输出。
EDIT2: 实际上,使用 Coliru(参见下面的 T.C.'s link),这个确切的代码 确实 产生双重删除错误。
如果显式调用析构函数,当 f3
超出范围时,它将被隐式调用第二次。这会创建 UB,这就是您的 class 崩溃的原因。
您可以通过在析构函数中将 s
重置为 nullptr
来解决 delete
中的崩溃(因此第二次是 nullptr
)但是 UB在两次调用析构函数时仍然存在。
对于任何具有非平凡析构函数的class,根据核心语言规则将其销毁两次是未定义的行为:
[basic.life]/p1:
The lifetime of an object of type
T
ends when:
- if
T
is a class type with a non-trivial destructor (12.4), the destructor call starts, or- the storage which the object occupies is reused or released.
[class.dtor]/p15:
the behavior is undefined if the destructor is invoked for an object whose lifetime has ended (3.8)
您的代码销毁了 f3
两次,一次是通过显式析构函数调用,一次是通过离开作用域,因此它具有未定义的行为。
碰巧 libstdc++ 和 libc++ 的 unique_ptr
析构函数都会将空指针分配给存储的指针(libc++ 调用 reset()
;libstdc++ 手动执行)。这不是标准所要求的,并且可以说是一个性能错误,它意味着对原始指针的零开销包装。因此,您的代码 "works" 在 -O0
中。
g++ 在 -O2
能够看到析构函数中的赋值不可能被定义良好的程序观察到,因此它优化了赋值,导致双重删除。