使用T[]访问相邻声明的两个T类型对象是否属于未定义行为?

5

我最近观看了Miro Kenjp在CppCon上的演讲,题为“不符合规范的C ++:委员会对您隐藏的秘密”。

在28分30秒处(https://youtu.be/IAdLwUXRUvg?t=1710),他指出访问相邻的double类型具有未定义行为,但我不太明白为什么。 有人能解释一下这是为什么吗?

如果是这种情况,那么以这种方式访问其他内容肯定也具有未定义行为,例如:

int* twoInts = malloc(sizeof(int) * 2);
int secondInt = twoInts[1]; //Undefined behaviour?

3
“int secondInt = twoInts[1]”是未定义行为(UB),但原因与视频中略有不同(我认为它可能不适用于C++20)。这是因为“malloc”实际上并没有创建任何东西,它只是分配内存。从技术上讲,您正在访问一个不存在的对象,这就是UB的原因。 - NathanOliver
@NathanOliver 那么在我的例子中,如果我在访问之前执行 twoInts [1] = 10,这应该是定义良好的行为吗?你能否详细解释一下视频中的原因? - name
3
不,这仍然将是未定义行为。要使上面的代码合法,您需要添加int * good_two_ints = new(twoInts) int[2]{};,然后继续使用good_two_ints而不是twoInts。这将使用放置new在malloc获取的内存中实际创建一个数组对象。 - NathanOliver
@NathanOliver 那么在这种情况下使用malloc创建数组是UB吗?虽然我知道C和C++是不同的语言,但我认为C++与C兼容,所以这应该没问题。 - name
2
@name 在 C++20 之前是非法的,因为根据该段落,特别是文本一个对象通过定义、通过new表达式、在隐式地更改联合的活动成员时,或者创建临时对象([conv.rval],[class.temporary])而被创建。 ,如果没有对象,则没有可供使用的对象,您所做的任何工作都将是未定义行为。 malloc 没有在其中提及,因此 malloc 不会创建对象。 - NathanOliver
显示剩余18条评论
2个回答

3
我想引用演示中的一句话来开始:
“你不是在与CPU编程,而是在与抽象机器编程”
你说:
“他指出访问相邻的double类型数据将产生未定义的行为(UB)。”
但你的引述不完整。他特别指出了这个非常关键的事实:
“…除非这些对象是数组的一部分。” malloc 是个红鲱鱼(以及一系列其他问题的沉重负担)。他的代码使用的是 new[],所以在这里使用 malloc 只会搞砸事情。
他在幻灯片上提到的具体问题是,在 buffer 上创建的 double 对象是通过 std::uninitialized_default_construct_n 创建的,该方法不会创建一个双精度浮点数数组,而是创建多个连续的对象。他断言,在 C++ 标准(你正在与之编程的抽象机器)中,除非你实际上创建了对象数组,否则不能将对象视为数组的一部分。
作者试图表达的观点是,C++ 标准存在缺陷,没有严格符合标准的方式来创建灵活的数组(在 C++20 之前)。
供参考,下面是代码(图像后重现):
struct header
{
    int size;
    byte* buffer;
    thing some;
};

constexpr size_t x = ...;

byte* buffer = new byte[x + n * sizeof(double)];
header* p = new (buffer) header{n, buffer};
uninitialized_default_construct_n(
    reinterpret_cast<double*>(buffer + x), n);
double* data = reinterpret_cast<double*>(p->buffer + x);

data[0] = data[1] + data[2]; // <-- problem here
                             // because we never created an array of doubles

我明白了,这基本上涵盖了我的大部分想法,只是你不能把对象视为数组的一部分,除非你实际创建了对象数组。我以为一个数组就是在内存中连续的对象。 - name
@name 是因为标准规定的。为什么?这是一个更长、更复杂的讨论。 - bolov
有没有可以阅读的来源,解释为什么这样做? - name
通常这样的别名规则对于编译器优化是很有帮助的。有了这样的规则,编译器可以推断出一个对象不能通过其他不相关的对象来访问。 - xskxzr

2

无法保证变量相邻分配。

  1. malloc 分配的内存区域可能与临时局部变量的内存区域不同。

  2. 局部变量可能不会存储在内存中,而是存储在寄存器中。

  3. 访问声明范围外的数组元素是未定义行为。
    数组可能被分配在内存末尾,因此访问数组之外的部分没有分配任何内存。

  4. 局部变量可能在另一个内存段(例如堆栈)中定义,而不是动态分配内存的内存。

以下是一个示例。
嵌入式系统具有芯片上内存和“芯片系统”(SOC)外的内存。芯片上的内存更快,但较少。架构师将此内存分配给堆栈。SOC 外部的内存速度较慢,但更多,因此将其分配给动态内存。

另一个示例:
操作系统支持虚拟内存。按需将内存分页到硬盘上。操作系统为您的程序分配了少量内存。少量内存将分配给堆栈,虚拟内存将分配给动态内存。

并非所有平台都由连续的内存组成。内存位置也可能分配给硬件设备。


1
但是在我描述的情况下(不确定视频中描述的情况),所访问的内存是由malloc分配的,这应该意味着它是连续的吗? - name
是的,但那不是我所指的。许多开发人员认为计算机具有一定范围的地址,而该范围内的所有内容都是内存(且连续的)。实际上,可能存在一些地址范围,其上没有任何分配或相关内容。 - Thomas Matthews

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