在C++中,将不同类型存储在一个内存块中是定义良好的行为吗

yduiuuwa  于 2023-04-08  发布在  其他
关注(0)|答案(2)|浏览(133)

长期以来,我一直想知道在一个连续的内存块中存储不同类型的项是否被标准很好地定义。
我已经有了使用malloc和指针算法来单独管理元素的同构容器的经验。
现在我想更进一步,我计划在一个块中连续存储几个不同的类型。
重要的是要忽略varianttag-union等替代方案,因为其目的是低级存储,以实现需要直接与内存接触以获得最大可能性能的解释器和虚拟机。
在下面的示例中,我存储了一个char,然后是一个int,最后是一个double

char* block = (char*)malloc(16);

//storage char

(char*) a = block;
*a = 'Z';

//storage int

(int*) b = block+4;

*b = 777;

//storage double

(double*) c = block+8;

*c = 3.1415;

free(block)

这在允许执行新编程语言的新解释器中使用是有用的,其中其规则允许示例化其成员在运行时定义的结构对象。

70gysomp

70gysomp1#

看起来很法律的,但是:
1.您应该使用sizeofalignof以可移植的方式计算偏移量。
1.你需要std::launder指针。像这样:std::launder(reinterpret_cast<T *>(block + i))
1.如果你的类型不是标量(不是implicit lifetime types),如果你没有使用C20或更新版本,你必须使用::new((void *)address) T开始对象生存期(注意,这里还不能使用std::launder)(如果你保留从new返回的指针,而不是单独重新计算它,你可以删除launder)。
在实践中,你可能会也可能不会不做[2](特别是在C
17之前,那里没有launder),也可能不会对可构造的类型(如标量)做[3],但从技术上讲,这将是UB。

5t7ly7z5

5t7ly7z52#

C20之前:嗯,也许吧?规则不清楚,而且相互矛盾。
在C
20及更高版本中:这很好
在C++20之前,关于对象创建和生存期的规则并没有写得很好。即使对于像这样简单的东西,也不清楚结果是否定义良好(即使这是malloc的正常和预期使用):

char* a = (char*)malloc(10);
a[0] = 'a';

所以每个人基本上都忽略了标准的确切措辞,并使这个工作,因为它显然应该。
C++20通过创建一个新的 implicit-lifetime 对象类别来修复标准的措辞。这些对象必须是 implicit-lifetime 类型:标量类型和类类型,它们是聚合类型或具有普通构造函数。
malloc现在被定义为在它分配的存储空间中隐式创建 implicit-lifetime 类型的对象。malloc隐式创建的对象的确切类型是未指定的。只要有一些类型集合使程序被良好定义,这就是它创建的对象类型。
在你最初的例子中,如果malloc在它返回的存储区域中创建了一个char、一个int和一个double,那么这个程序将是定义良好的,所以这就是我们所说的它隐式地做了什么。
请记住,以上所有内容仅适用于普通类型。对于任何需要非普通构造的内容,您将需要使用placement-new在malloc返回的存储中创建对象。
当然,也没有真实的的理由不使用placement-new来初始化普通类型,所以你可能会更好地回避整个问题,只使用placement-new来初始化所有类型。例如,你最初的例子可以像这样重写,以避免所有这些语言-法律问题:

template <typename T>
std::size_t align_for(std::size_t n) {
    return n % alignof(T) == 0 ? 0 : alignof(T) - (n % alignof(T));
}

int main() {
    std::size_t size = 1;  // For the char
    size += align_for<int>(size) + sizeof(int);
    size += align_for<double>(size) + sizeof(double);
    size += align_for<std::string>(size) + sizeof(std::string);
    char* block = reinterpret_cast<char*>(std::malloc(size));
    std::size_t next = 0;
    
    //store char
    char* a = new (block + next) char('Z');
    next += 1;
    
    //store int
    next += align_for<int>(next);
    int* b = new (block + next) int(777);
    next += sizeof(int);
    
    //store double
    next += align_for<double>(next);
    double* c = new (block + next) double(3.1415);
    next += sizeof(double);
    
    //store non-trivial type: std::string
    next += align_for<std::string>(next);
    std::string* d = new (block + next) std::string("hello world");
    next += sizeof(std::string);
    
    //Note: must manually call the destructor of non-trivial types,
    //      but it's also harmless to do so for trivial types as well
    std::destroy_at(a);
    std::destroy_at(b);
    std::destroy_at(c);
    std::destroy_at(d);
    
    std::free(block);
}

Demo
注意,在这个例子中,我有:
1.使用alignofsizeof以可移植的方式计算缓冲区的大小和对象在其中的位置。
1.使用placement-new来确保对象被正确地创建和初始化。这对于 implicit-lifetime 类型来说不是必需的,但它也没有害处,并且对于非平凡类型是必需的。
1.显式销毁所有内容。同样,这对于平凡类型不是必需的,但在其他情况下是必需的。显式销毁平凡类型并没有什么坏处(并且调用可能不会导致编译器生成额外的代码)。

相关问题