std::unique_ptr<T[]> API 禁止从派生类指针进行向基类指针的转换。

14
在《现代高效C++》的"Iterm 19: Use std::shared_ptr for shared-ownership resource management."一章中,第133-134页写道:
std::shared_ptr支持从派生类指针向基类指针的转换,这对于单个对象是有意义的,但当应用于数组时会在类型系统中留下漏洞。(因此,std::unique_ptr API禁止这种转换。)
“在类型系统中留下漏洞”的含义是什么?
为什么std::unique_ptr API会禁止从派生类指针向基类指针的转换?
它是如何禁止这些转换的?

8
Derived类型的数组并不是Base类型数组的一种。 - selbie
其实,我打算放弃回答,因为(a)它是重复问题,(b)因为我不认为它回答了关于类型系统漏洞的具体问题。 - WhozCraig
@WhozCraig 你为什么认为你的回答没有解决问题?我发现的潜在重复问题的答案没有提到底层问题,即内存布局,而你很好地解释了这个问题。你只是错过了禁止转换的机制,其中clang对于unique_ptr似乎明确使用SFINAE https://godbolt.org/z/LGfULn - luk32
1个回答

20
类型系统漏洞是指编译器没有捕获当一个类型被强制转换为另一个不兼容类型的情况。假设有两个简单类:
class A
{
    char i;
};

class B : public A
{
    char j;
};

为了简化,我们忽略像填充等内容,并假设类型为A的对象占用1个字节,类型为B的对象占用2个字节。

现在,当您有一个类型为A或类型为B的数组时,它们将如下所示:

A a[4]:

=================
| 0 | 1 | 2 | 3 |
|-------|-------|
| i | i | i | i |
=================

B b[4]:

=================================
|   0   |   1   |   2   |   3   |
|-------|-------|-------|-------|
| i | j | i | j | i | j | i | j |
=================================

现在想象一下,您有指向这些数组的指针,然后将一个指针转换为另一个指针,这显然会导致问题:

a cast to B[4]:

=================================
|   0   |   1   |   2   |   3   |
|-------|-------|-------|-------|
| i | j | i | j | x | x | x | x |
=================================

数组中的前两个对象将解释第二个和第四个 Ai 成员作为它们的 j 成员。第二个和第三个成员访问未分配的内存。
b cast to A[4]:

=================
| 0 | 1 | 2 | 3 |
|-------|-------|
| i | i | i | i | x | x | x | x |
=================

这里情况正好相反,所有4个对象现在交替地将2个实例的j解释为它们自己的成员。而且数组的一半丢失了。

现在想象一下删除这样一个转换后的数组。哪些析构函数会被调用?哪些内存将被释放?此时你已经深陷困境。

但是,还有更多。

假设你有三个类类似于这样:

class A
{
    char i;
};

class B1 : public A
{
    float j;
};

class B2 : public A
{
    int k;
};

现在你需要创建一个B1指针数组:

B1* b1[4];

如果你将该数组转换为一个指向A类型指针的数组,你可能会认为,"嗯,这很好,对吧"

A** a = <evil_cast_shenanigans>(b1);

我的意思是,您可以将每个成员安全地作为指向A的指针进行访问:

char foo = a[0]->i; // This is valid

但是你还可以做这个:

a[0] = new B2{};   // Uh, oh.

这是一个有效的赋值,没有编译器会抱怨,但你不应该忘记我们实际上正在处理作为指向 B1 对象指针数组创建的数组。它的第一个成员现在指向一个 B2 对象,你现在可以访问它并将其视为 B1,而编译器却不会有任何提示。

float bar = b1[0]->j;   // Ouch.

所以您又陷入麻烦了,如果首先不允许这种向上转换,编译器就无法警告您。

为什么std::unique_ptr API会禁止从派生类指针向基类指针的转换?

我希望上面的解释给出了很好的理由。

它如何禁止这些转换?

它只是不提供任何进行转换的API。shared_ptr API有像static_pointer_cast这样的转换函数,而unique_ptr API没有。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接