将基本类型数组中的内存重用于其他(但仍是基本)类型数组是否合法?

时间:2018-08-20 13:37:43

标签: c++ casting language-lawyer strict-aliasing

这是其他question关于内存重用的后续措施。由于最初的问题与特定的实现有关,所以答案与该特定的实现有关。

所以我想知道,在一致的实现中,将基本类型的数组的内存重新用于提供的不同类型的数组是否合法:

  • 这两种类型都是基本类型,因此具有琐碎的dtor和默认的ctor
  • 两种类型都具有相同的大小和对齐要求

我以以下示例代码结尾:

DELETE

我添加了注释,解释了为什么我认为这段代码应该定义了行为。恕我直言,一切都很好并且符合AFAIK标准,但是我无法找到标有 question 的行是否有效。

float对象确实从int对象中重用了内存,因此int的生存时间在float的生存时间开始时结束,因此stric-alias规则应该没有问题。数组是动态分配的,因此对象(整数和浮点数)实际上都是在#include <iostream> constexpr int Size = 10; void *allocate_buffer() { void * buffer = operator new(Size * sizeof(int), std::align_val_t{alignof(int)}); int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok for (int i=0; i<Size; i++) in[i] = i; // Defined behaviour because int is a fundamental type: // lifetime starts when is receives a value return buffer; } int main() { void *buffer = allocate_buffer(); // Ok, defined behaviour int *in = static_cast<int *>(buffer); // Defined behaviour since the underlying type is int * for(int i=0; i<Size; i++) { std::cout << in[i] << " "; } std::cout << std::endl; static_assert(sizeof(int) == sizeof(float), "Non matching type sizes"); static_assert(alignof(int) == alignof(float), "Non matching alignments"); float *out = static_cast<float *>(buffer); // (question here) Declares a dynamic float array starting at buffer // std::cout << out[0]; // UB! object at &out[0] is an int and not a float for(int i=0; i<Size; i++) { out[i] = static_cast<float>(in[i]) / 2; // Defined behaviour, after execution buffer will contain floats // because float is a fundamental type and memory is re-used. } // std::cout << in[0]; // UB! lifetime has ended because memory has been reused for(int i=0; i<Size; i++) { std::cout << out[i] << " "; // Defined behaviour since the actual object type is float * } std::cout << std::endl; return 0; } 返回的 void类型数组中创建的。所以我认为一切都应该没事。

但是由于它允许低级对象替换,这在现代C ++中通常是不受欢迎的,所以我必须承认我有疑问...

所以问题是:上面的代码是否调用UB,如果是,则在何处以及为什么?

免责声明:我建议在可移植代码库中反对此代码,这实际上是一个语言律师问题。

3 个答案:

答案 0 :(得分:9)

int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok

正确。但可能并非您所期望的那样。 [expr.static.cast]

  

类型为“指向cv1 void的指针的prvalue可以转换为类型为”指向cv2 T的指针的prvalue,其中T是对象类型,cv2cv1的简历资格相同或更高。如果原始指针值表示存储器中字节的地址A,并且A不满足T的对齐要求,则未指定结果指针值。否则,如果原始指针值指向对象a,并且存在类型为b(忽略cv限定)的对象T,该指针可以与{{1}进行指针转换},结果是指向a的指针。否则,转换后指针值将保持不变。

b上没有int或指针可互换的对象,因此指针值未更改。 buffer是类型为in的指针,指向原始内存区域。

int*

不正确。 [intro.object]

  

在隐式更改联合的活动成员时或在创建临时对象时,将通过定义,new-expression创建对象。

值得注意的是分配。没有创建for (int i=0; i<Size; i++) in[i] = i; // Defined behaviour because int is a fundamental type: // lifetime starts when is receives a value 。实际上,通过消除,intinvalid pointer,而取消引用它是UB。

后面的in也都作为UB。

即使没有通过正确使用float*创建对象而存在上述所有UB,也不存在 array 对象。 (不相关的)对象恰好在内存中并排。这意味着指针算术与结果指针也为UB。 [expr.add]

  

将具有整数类型的表达式添加到指针或从指针中减去时,结果将具有指针操作数的类型。如果表达式new (pointer) Type{i};指向具有P个元素的数组对象x[i]的元素x,则表达式nP + J(其中J具有值j)指向(可能是假设的)元素J + P,否则,行为是不确定的。同样,表达式x[i+j] if 0 ≤ i+j ≤ n;指向(可能是假设的)元素P - J,否则,行为是不确定的。

假设元素指的是一个过去(假设)元素。请注意,指向恰好与另一个对象位于相同地址位置的一个end-the-end元素的指针不会指向该另一个对象。

答案 1 :(得分:5)

Passer By的答案涵盖了示例程序为何具有未定义行为的原因。我将尝试回答如何在不使用UB的情况下以最小的UB重用而无需UB (鉴于标准的当前措辞,在标准C ++中,数组的存储重用在技术上是不可能的,因此要实现重用,程序员必须依靠实施来“做正确的事”。

转换指针不会自动将对象体现为存在。您必须首先构造float对象。 开始它们的生命周期,并结束int对象的生命周期(对于非平凡对象,首先需要调用析构函数):

Selenium

您可以直接使用由placement new返回的指针(在我的示例中被丢弃)直接使用刚构造的for(int i=0; i<Size; i++) new(in + i) float; 对象,也可以float std::launder指针:< / p>

buffer

但是,重用类型float *out = std::launder(reinterpret_cast<float*>(buffer)); (或unsigned char)的存储而不是std::byte对象的存储,是更常见的。

>

答案 2 :(得分:0)

我之所以加入,是因为我觉得至少有一个未回答的问题,这个问题没有被大声说出来,如果那不是真的,我们深表歉意。我认为这些家伙巧妙地回答了这个问题的主要问题:何处以及为什么这是不确定的行为; user2079303给出了一些解决办法。我将尝试回答如何修复代码以及为什么有效的问题。在开始阅读我的帖子之前,请阅读“ Passer By”和“ user2079303”的答案下的答案和评论讨论。

基本上,问题是对象不存在,即使它们确实不需要任何东西(除了存储空间)才能存在。在标准的生命周期部分对此进行了说明,但是在声明之前,在C ++对象模型部分中

  

在隐式更改联合的活动成员(12.3)或创建临时对象(7.4、15.2)时,将通过定义(6.1),新表达式(8.3.4)创建对象。

对象概念的定义有些棘手,但是很有意义。 proposal Implicit creation of objects for low-level object manipulation中更精确地解决了该问题,以简化对象模型。在此之前,我们应该通过上述方式显式创建对象。在这种情况下,可以使用的一种方法是new-placement表达式,new-placement是一个非分配的new-expression,它创建一个对象。对于这种特殊情况,这将有助于我们创建缺少的数组对象和浮动对象。下面的代码显示了我的想法,包括一些与行相关的注释和汇编说明(使用了clang++ -g -O0)。

constexpr int Size = 10;

void* allocate_buffer() {

  // No alignment required for the `new` operator if your object does not require
  // alignment greater than alignof(std::max_align_t), what is the case here
  void* buffer = operator new(Size * sizeof(int));
  // 400fdf:    e8 8c fd ff ff          callq  400d70 <operator new(unsigned long)@plt>
  // 400fe4:    48 89 45 f8             mov    %rax,-0x8(%rbp)


  // (was missing) Create array of integers, default-initialized, no
  // initialization for array of integers
  new (buffer) int[Size];
  int* in = reinterpret_cast<int*>(buffer);
  // Two line result in a basic pointer value copy
  // 400fe8:    48 8b 45 f8             mov    -0x8(%rbp),%rax
  // 400fec:    48 89 45 f0             mov    %rax,-0x10(%rbp)


  for (int i = 0; i < Size; i++)
    in[i] = i;
  return buffer;
}

int main() {

  void* buffer = allocate_buffer();
  // 401047:    48 89 45 d0             mov    %rax,-0x30(%rbp)


  // static_cast equivalent in this case to reinterpret_cast
  int* in = static_cast<int*>(buffer);
  // Static cast results in a pointer value copy
  // 40104b:    48 8b 45 d0             mov    -0x30(%rbp),%rax
  // 40104f:    48 89 45 c8             mov    %rax,-0x38(%rbp)


  for (int i = 0; i < Size; i++) {
    std::cout << in[i] << " ";
  }
  std::cout << std::endl;
  static_assert(sizeof(int) == sizeof(float), "Non matching type sizes");
  static_assert(alignof(int) == alignof(float), "Non matching alignments");
  for (int i = 0; i < Size; i++) {
    int t = in[i];


    // (was missing) Create float with a direct initialization
    // Technically that is reuse of the storage of the array, hence that array does
    // not exist anymore.
    new (in + i) float{t / 2.f};
    // No new is called
    // 4010e4:  48 8b 45 c8             mov    -0x38(%rbp),%rax
    // 4010e8:  48 63 4d c0             movslq -0x40(%rbp),%rcx
    // 4010ec:  f3 0f 2a 4d bc          cvtsi2ssl -0x44(%rbp),%xmm1
    // 4010f1:  f3 0f 5e c8             divss  %xmm0,%xmm1
    // 4010f5:  f3 0f 11 0c 88          movss  %xmm1,(%rax,%rcx,4)


    // (was missing) Create int array on the same storage, default-initialized, no
    // initialization for an array of integers
    new (buffer) int[Size];
    // No code for new is generated
  }


    // (was missing) Create float array, default-initialized, no initialization for an array
    // of floats
  new (buffer) float[Size];
  float* out = reinterpret_cast<float*>(buffer);
  // Two line result in a simple pointer value copy
  // 401108:    48 8b 45 d0             mov    -0x30(%rbp),%rax
  // 40110c:    48 89 45 b0             mov    %rax,-0x50(%rbp)


  for (int i = 0; i < Size; i++) {
    std::cout << out[i] << " ";
  }
  std::cout << std::endl;
  operator delete(buffer);
  return 0;
}

基本上,即使使用-O0,在机器代码中也将省略所有新放置的表达式。使用GCC -O0实际上operator new被调用,而使用-O1也被省略。让我们暂时忽略该标准的形式,并从实际意义上直接思考。为什么我们实际上需要调用什么都不做的函数,没有什么可以阻止没有这些功能的函数,对吗?因为C ++正是将对内存的整体控制权交给了程序的语言,而不是某些运行时库或虚拟机等的语言。我在这里可能会想到的原因之一是,该标准再次为编译器提供了更多的优化限制该程序要采取一些额外的措施。想法可能是编译器可以执行任何重新排序,而只知道定义,new-expression,union,临时对象作为指导优化算法的新对象提供程序的机器代码就可以省略魔术。如果您分配了内存并且没有为琐碎的类型调用new运算符,那么在现实中很可能没有这样的优化会破坏您的代码。有趣的事实是new operator的那些非分配版本是保留的,不允许替换,这可能恰恰是最简单的形式,它告诉编译器一个新对象。