在单个分配中构造对象的法律的方法是什么(C++17)

62lalag4  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(78)

有一个相关的问题,Is this a correct way to store different types in the same allocation?,但我想知道答案是否完全法律的。我猜这个主题比看起来更复杂.我的问题是关于下面的片段,其中可能不同类型的多个对象以简单的方式在单个分配中使用放置new构造。

#include<cstdlib>
#include<new>

struct A {
  int a;
  int b;
};

struct B {
  int c;
};

int main() {
  void* storage = std::malloc(1024);
  char* ptr = reinterpret_cast<char*>(storage);
  ptr += 16;
  new(ptr) A;
  ptr += sizeof(A);
  // construct other objects in the same way
  new(ptr) B;
}

字符串
首先,根据"constructing" a trivially-copyable object with memcpy中的答案,P0593追溯适用。
Shafik的回答正确地引用了p0593,在回答的时候是一个建议。但是从那时起,这个建议被接受了,事情得到了定义。
这意味着必须隐式地创建一个char数组来给予reinterpret_cast定义的行为。因此,指针算术ptr += 16也定义得很好。
new(ptr) A替换了数组中从ptrptr + sizeof(A) - 1的部分,* 结束了在该范围内隐式创建的char元素的生存期 *。所以我想说,在这一点上,ptr在技术上指向生存期已经结束的对象,并且连续指针算术ptr += sizeof(A)是UB。
到目前为止,我是正确的吗?如果是这样,可能的解决方案如下所示:
(BTW,如果这是真的,那表示Is this a correct way to store different types in the same allocation?的答案仍然有UB(因为在new之后递增指针)。

  1. std::launder
new(ptr) A;
// std::launder(ptr) is now pointing to the existing `A` instance as char*.
// This is file because a char pointer can alias anything.
ptr = std::launder(ptr) + sizeof(A); 
// ptr += 16; // UB


然而,这似乎仍然可以有UB,因为 *std::launder(ptr)别名A对象,但不指向任何数组对象 *。非数组对象上的指针运算是UB。因此,如果我在这些行之后做了,例如,ptr += 128,它将是UB。参见Type punning in modern C++ - Timur Doumler - CppCon 2019(from 44:10)
1.递增ptr,然后递增new

ptr += sizeof(A); // Increment the pointer beforehand so that ptr doesn't point to dead objects.
new(ptr - sizeof(A)) A;
// `ptr` is pointing to an existing char element of the implicitly created ARRAY, 
// so it's fine to increment `ptr` after this.


1.构造对象,然后将其std::memcpy到存储中
我相信这也是一个简单而明确的解决办法。
所以我猜可行的解决方案是2或3,我的理解是正确的吗?谢谢。

plupiseo

plupiseo1#

让我看看我是否理解了你的问题,你想知道在同一个内存块中创建两种不同类型的对象是否可能并且是法律的,这两种对象沿着这个内存块分布。
答案是肯定的,但你必须考虑一些重要的方面:
1.您必须在执行此操作时分配内存
1.必须使用placement new运算符初始化示例。
1.在使用placement new操作符后,必须为每个示例显式调用析构函数。
使用你的代码,这是一个过程:

struct A {
  int a;
  int b;
  ~A(){cout << "~A{" << a << "," << b <<"}\n";} 
};

struct B {
  int c;
  ~B(){cout << "~B{" << c << "}\n";}
};

struct C {
  char* s;
  C(const char* si){        
        s = new char[256];
        size_t i{0};
        while(si && si[i]!='\0')
        {
            s[i] = si[i];
            i++;
        }
        cout << "C(" << s << ")\n";
    }
  ~C(){        
        cout << "~C{" << s << "}\n";
        delete[] s;
    }
};

int main()
{
    void* storage = (char*)std::malloc(1024);
    char* ptrA = reinterpret_cast<char*>(storage);;
    char* ptrB = ptrA + sizeof(A);
    char* ptrC = ptrB + sizeof(B);

    cout <<  "SA: " << sizeof(A) << "\n";
    cout <<  "SB: " << sizeof(B) << "\n";
    cout <<  "SB: " << sizeof(C) << "\n";

    A* a = ::new(ptrA) A{23,6463}; // new(prt) is the placement new operator 
    B* b = ::new(ptrB) B{333};
    C* c = ::new(ptrC) C("This is my text");

    cout << "A.a: " << a->a << "A.b: " << a->b << "\n"; 
    cout << "B.a: " << b->c << "\n"; 
    cout << "C.s: " << c->s << "\n";
    
    a->~A();
    b->~B();
    c->~C();
    
    free(storage);

    return 0;
}

字符串
输出值:

A: 8
SB: 4
SB: 8
C(This is my text)
A.a: 23A.b: 6463
B.a: 333
~A{23,6463}
~B{333}
~C{This is my text}


如果在指针a上使用delete,则可能会获得双重自由检测的异常。

delete a;
    //delete b;
    //delete c;

    //a->~A();
    //b->~B();
    //c->~C();
    
    free(storage);


输出量:

A: 8
SB: 4
SB: 8
C(This is my text)
A.a: 23A.b: 6463
B.a: 333
C.s: This is my text
~A{23,6463}
Program stderr
free(): double free detected in tcache 2
Program terminated with signal: SIGSEGV


如果你试图从另一个不同于指针a的指针释放内存,你会有一个异常,

//delete a;
    delete b;
    //delete c;


输出量:

munmap_chunk(): invalid pointer
Program terminated with signal: SIGSEGV


正如@ChrisDodd所说,运算符newnew(ptr)之间有很大的区别。new(ptr)placement new,这意味着我们可以传递预分配的内存并在该内存块中构造对象。在我的示例中,'::'确保解析发生在全局命名空间中,以避免拾取operator new的重叠版本
.这里是另一个链接阅读更多关于这个主题:
https://www.geeksforgeeks.org/placement-new-operator-cpp/
有关更多信息,我推荐Björn Andirst和Viktor Sehr的《C++ High Performance》一书,在内存管理部分。

相关问题