这应该被称为对象切片的一些特殊情况吗?

Posted

技术标签:

【中文标题】这应该被称为对象切片的一些特殊情况吗?【英文标题】:Should this be called some special case of object slicing? 【发布时间】:2016-08-31 06:38:43 【问题描述】:

假设我有一个类Derived,它派生自类Base,而sizeof(Derived) > sizeof(Base)。现在,如果像这样分配Derived 的数组:

Base * myArray = new Derived[42];

然后尝试使用

访问n-th 对象
doSomethingWithBase(myArray[n]);

那么,由于从无效位置访问Base,这可能(但并非总是)导致未定义的行为。

这种编程错误的正确说法是什么? 是否应该将其视为object slicing 的情况?

【问题讨论】:

@Slava 这是经典的动态 C 风格 数组。 C 风格的数组是通过指向其第一个元素的原始指针公开的对象序列。如何分配并不重要。 @n.m. vector如何在里面分配数据?它正在使用 new[] 不是吗?但据你说这是程序员的错误 @Slava 这与OP的问题无关;请避免在这里问。 @Slava how vector allocates data inside 不是任何人的事情。它由标准保证工作,这是所有用户需要知道的。 new,如果使用,由标准库调用。允许标准库使用图书馆用户无法访问或不建议使用的东西。 作为德国人,我建议将 Arrayelementsizemismatch 作为正确的术语。嗯。必须在某处推一个“基地”...... 【参考方案1】:

它根本不是切片,而是未定义的行为,因为您正在访问一个不存在的 Derived 对象(除非您很幸运并且尺寸对齐,在这种情况下它仍然是 UB,但可能会做一些有用的事情无论如何)。

这是一个简单的指针运算失败的例子。

【讨论】:

即使您“走运并且尺寸对齐”,它仍然是未定义的行为。指针是指针,而不是整数。从技术上讲。 :) @LightnessRacesinOrbit:你说得对,我的措辞让它听起来在这种情况下完全有效,但事实并非如此。我已经更新了答案以反映这一点。【参考方案2】:

这不是切片的情况,尽管它非常相似。切片定义明确。由于非法指针运算,这只是未定义的行为(总是,不仅仅是可能的)。

【讨论】:

【参考方案3】:

这绝不是对象切片。

C++ 标准完美地定义了对象切片。这可能违反了面向对象的设计原则或其他什么,但不违反 C++ 规则。

此代码违反5.7 [expr.add] paragraph 7:

对于加法或减法,如果表达式PQ 的类型为“指向cv T 的指针”,其中T 不同于cv-unqualified 数组元素类型,行为未定义。 [注意:特别是,当数组包含派生类类型的对象时,指向基类的指针不能用于指针算术。 ——尾注]。

数组下标运算符定义为等价于指针运算,5.2.1 [expr.sub] paragraph 1:

表达式E1[E2]*((E1)+(E2)) 相同(根据定义)

【讨论】:

+1。有趣的是,这意味着即使myArray + 0 也会调用未定义的行为。我想知道目前是否有任何编译器使用该事实进行任何奇怪的优化? @ruakh:他们可以使用它来确定动态类型是否等于静态类型并消除虚函数调用。不知道,如果有的话。 “C++ 标准完美定义了对象切片”是什么意思 @jotik 我的意思是对象切片不违反任何 C++ 规则。没有未定义的行为。 @MikeMB 不错,这是向编译器提示对象的动态类型的绝佳方式。【参考方案4】:

这不是对象切片。

如前所述,索引myArray 不会导致对象切片,但会导致由于索引Derived 数组而导致的未定义行为,就好像它是Base 数组一样。

一种“数组衰减错误”。

new Derived[42] 分配给myArray 时引入的错误可能是数组衰减错误的变体。

在此类错误的真实实例中,存在一个实际数组:

Derived x[42];
Base *myArray = x;

问题的出现是因为Derived 的数组衰减为指向Derived 的指针,其值等于其第一个元素的地址。衰减允许指针分配正常工作。这种衰减行为继承自 C,这是一种允许数组“通过引用传递”的语言设计特性。

这导致我们更糟糕地体现了这个错误。此功能为数组语法提供了 C 和 C++ 语义,将数组函数参数转换为指针参数的别名。

void foo (Base base_array[42]) 
    //...


Derived d[42];
foo(d);          // Boom.

但是,new[] 实际上是一个重载运算符,它返回一个指向已分配数组对象开头的指针。所以它不是数组衰减的真实实例(即使使用了数组分配器)。不过bug症状是一样的,new[]的意图是得到Derived的数组。

检测并避免错误。

使用智能指针。

这种问题可以通过使用智能指针对象而不是管理原始指针来避免。例如,unique_ptr 的类似编码错误如下所示:

std::unique_ptr<Base[]> myArray = new Derived[42];

这会产生编译时错误,因为unique_ptrs 构造函数是explicit

使用容器,也许是std::reference

或者,您可以避免使用new[],而使用std::vector&lt;Derived&gt;。然后,您将强迫自己设计一个不同的解决方案,将这个数组发送到只知道Base 的框架代码。可能是模板函数。

void my_framework_code (Base &object) 
    //...


template <typename DERIVED>
void my_interface(std::vector<DERIVED> &v) 
    for (...) 
        my_framework_code(v[i]);
    

或者,使用std::reference_wrapper&lt;Base&gt;

std::vector<Derived> v(42);
std::vector<std::reference_wrapper<Base>> myArray(v.begin(), v.end());

【讨论】:

如果尺寸相同,它很可能会起作用。标准对此有什么要说的吗? @sp2danny:一旦执行了数组到指针的转换,Base 指针必须被视为指向非数组对象的指针。只有Derived 指针可以被视为指向数组对象的指针。因此,Base 指针上的指针运算仅限于为长度为 1 的数组定义的内容。 @jxh 所以与 ruakh 对 n.m. 的回答的评论相反,myArray + 0 不是未定义的行为? @sp2danny:好的,在 C++2014 中找到了 n.m. 的引用。

以上是关于这应该被称为对象切片的一些特殊情况吗?的主要内容,如果未能解决你的问题,请参考以下文章

python基础篇06-元组/字典

python特殊方法

特殊情况下复制恢复引起的调用混乱

闭包会造成内存泄漏吗?

Python类的特殊方法

07 Python初学(元组)