将 shared_ptr<T> 向上转换为 shared_ptr<void> 会导致未定义的行为吗?
Can upcasting a shared_ptr<T> to a shared_ptr<void> lead to undefined behaviour?
共享指针非常聪明。他们记住他们最初构造的类型,以便正确删除它们。以此为例:
struct A { virtual void test() = 0; };
struct B : A { void test() override {} };
void someFunc() {
std::shared_ptr<A> ptr1;
ptr1 = std::make_shared<B>();
// Here at the end of the scope, B is deleted correctly
}
但是,void 指针似乎存在一个问题:要使 void 指针的向下转换有效,必须将其向下转换为最初向上转换的类型。
例如:
void* myB = new B;
// Okay, well defined
doStuff(static_cast<B*>(myB));
// uh oh, not good!
// For the same instance of a child object, a pointer to the base and
// a pointer to the child can be differrent.
doStuff(static_cast<A*>(myB));
使用std::shared_ptr
,当你使用std::make_shared
时,删除器必须类似于这个函数:[](B* ptr){ delete ptr; }
。由于指针(在第一个示例中)在指向 A
的指针中保存了一个 B
实例并正确删除了它,因此它必须以某种方式向下转型。
我的问题是:下面的代码片段是否调用了未定义的行为?
void someFunc() {
{
std::shared_ptr<void> ptr = std::make_shared<B>();
// Deleting the pointer seems okay to me,
// the shared_ptr knows that a B was originally allocated with a B and
// will send the void pointer to the deleter that's delete a B.
}
std::shared_ptr<void> vptr;
{
std::shared_ptr<A> ptr = std::make_shared<B>();
// ptr is pointing to the base, which can be
// different address than the pointer to the child.
// assigning the pointer to the base to the void pointer.
// according to my current knowledge of void pointers,
// any future use of the pointer must cast it to a A* or end up in UB.
vptr = ptr;
}
// is the pointer deleted correctly or it tries to
// cast the void pointer to a B pointer without downcasting to a A* first?
// Or is it well defined and std::shared_ptr uses some dark magic unknown to me?
}
代码正确。
std::shared_ptr
在构造函数内部保存了真正的指针和真正的删除器,所以无论你如何向下转换它,只要向下转换有效,删除器就是正确的。
shared_ptr
实际上并不保存指向对象的指针,而是指向保存实际对象、引用计数器和删除器的中间结构的指针。转换 shared_ptr
并不重要,中间结构不会改变。它无法更改,因为您的 vptr
和 ptr
尽管类型不同,但共享引用计数器(当然还有对象和删除器)。
顺便说一句,中间结构是 make_shared
优化的原因:它在同一个内存块中分配中间结构和对象本身,避免额外分配。
为了说明智能指针的好处,我编写了一个由于您的问题而崩溃(使用 GCC 6.2.1)的普通指针程序:
#include <memory>
#include <iostream>
struct A
{
int a;
A() :a(1) {}
~A()
{
std::cout << "~A " << a << std::endl;
}
};
struct X
{
int x;
X() :x(3) {}
~X()
{
std::cout << "~X " << x << std::endl;
}
};
struct B : X, A
{
int b;
B() : b(2) {}
~B()
{
std::cout << "~B " << b << std::endl;
}
};
int main()
{
A* a = new B;
void * v = a;
delete (B*)v; //crash!
return 0;
}
实际上它打印了错误的整数值,这证明了 UB。
~B 0
~A 2
~X 1
*** Error in `./a.out': free(): invalid pointer: 0x0000000001629c24 ***
但是带有智能指针的版本工作得很好:
int main()
{
std::shared_ptr<void> vptr;
{
std::shared_ptr<A> ptr = std::make_shared<B>();
vptr = ptr;
}
return 0;
}
它按预期打印:
~B 2
~A 1
~X 3
shared_ptr
总是将原始指针传递给删除器,而不是通过vptr.get()
获得的指针。这不仅是为了使这种情况有效,而且还有指向成员子对象和拥有对象的指针,如构造函数重载 shared_ptr<T>::shared_ptr(const shared_ptr<T>&, element_type*)
.
中所体现的那样
所以这是安全的。
共享指针非常聪明。他们记住他们最初构造的类型,以便正确删除它们。以此为例:
struct A { virtual void test() = 0; };
struct B : A { void test() override {} };
void someFunc() {
std::shared_ptr<A> ptr1;
ptr1 = std::make_shared<B>();
// Here at the end of the scope, B is deleted correctly
}
但是,void 指针似乎存在一个问题:要使 void 指针的向下转换有效,必须将其向下转换为最初向上转换的类型。
例如:
void* myB = new B;
// Okay, well defined
doStuff(static_cast<B*>(myB));
// uh oh, not good!
// For the same instance of a child object, a pointer to the base and
// a pointer to the child can be differrent.
doStuff(static_cast<A*>(myB));
使用std::shared_ptr
,当你使用std::make_shared
时,删除器必须类似于这个函数:[](B* ptr){ delete ptr; }
。由于指针(在第一个示例中)在指向 A
的指针中保存了一个 B
实例并正确删除了它,因此它必须以某种方式向下转型。
我的问题是:下面的代码片段是否调用了未定义的行为?
void someFunc() {
{
std::shared_ptr<void> ptr = std::make_shared<B>();
// Deleting the pointer seems okay to me,
// the shared_ptr knows that a B was originally allocated with a B and
// will send the void pointer to the deleter that's delete a B.
}
std::shared_ptr<void> vptr;
{
std::shared_ptr<A> ptr = std::make_shared<B>();
// ptr is pointing to the base, which can be
// different address than the pointer to the child.
// assigning the pointer to the base to the void pointer.
// according to my current knowledge of void pointers,
// any future use of the pointer must cast it to a A* or end up in UB.
vptr = ptr;
}
// is the pointer deleted correctly or it tries to
// cast the void pointer to a B pointer without downcasting to a A* first?
// Or is it well defined and std::shared_ptr uses some dark magic unknown to me?
}
代码正确。
std::shared_ptr
在构造函数内部保存了真正的指针和真正的删除器,所以无论你如何向下转换它,只要向下转换有效,删除器就是正确的。
shared_ptr
实际上并不保存指向对象的指针,而是指向保存实际对象、引用计数器和删除器的中间结构的指针。转换 shared_ptr
并不重要,中间结构不会改变。它无法更改,因为您的 vptr
和 ptr
尽管类型不同,但共享引用计数器(当然还有对象和删除器)。
顺便说一句,中间结构是 make_shared
优化的原因:它在同一个内存块中分配中间结构和对象本身,避免额外分配。
为了说明智能指针的好处,我编写了一个由于您的问题而崩溃(使用 GCC 6.2.1)的普通指针程序:
#include <memory>
#include <iostream>
struct A
{
int a;
A() :a(1) {}
~A()
{
std::cout << "~A " << a << std::endl;
}
};
struct X
{
int x;
X() :x(3) {}
~X()
{
std::cout << "~X " << x << std::endl;
}
};
struct B : X, A
{
int b;
B() : b(2) {}
~B()
{
std::cout << "~B " << b << std::endl;
}
};
int main()
{
A* a = new B;
void * v = a;
delete (B*)v; //crash!
return 0;
}
实际上它打印了错误的整数值,这证明了 UB。
~B 0
~A 2
~X 1
*** Error in `./a.out': free(): invalid pointer: 0x0000000001629c24 ***
但是带有智能指针的版本工作得很好:
int main()
{
std::shared_ptr<void> vptr;
{
std::shared_ptr<A> ptr = std::make_shared<B>();
vptr = ptr;
}
return 0;
}
它按预期打印:
~B 2
~A 1
~X 3
shared_ptr
总是将原始指针传递给删除器,而不是通过vptr.get()
获得的指针。这不仅是为了使这种情况有效,而且还有指向成员子对象和拥有对象的指针,如构造函数重载 shared_ptr<T>::shared_ptr(const shared_ptr<T>&, element_type*)
.
所以这是安全的。