2

我开始实现一个基于 ID 的内存池,其中每个元素都有一个 id,它基本上是向量中的索引。在这种特殊情况下,我在构造对象本身之前就知道索引,所以我认为我在调用构造函数之前设置了 ID。

一些细节

从基于 ID 的池中分配对象如下:

  1. 从池中分配一个空闲 id
  2. 根据 id 值获取内存地址
  3. 在内存地址上构造对象
  4. 设置对象的ID成员

并且释放是基于该 id

这是代码(感谢jrok):

#include <new>
#include <iostream>

struct X
{
  X()
  {
    // id come from "nothing"
    std::cout << "X constructed with id: " << id << std::endl;
  }
  int id;
};

int main()
{
    void* buf = operator new(sizeof(X));

    // can I set the ID before the constructor call
    ((X*)buf)->id = 42;
    new (buf) X;
    std::cout << ((X*)buf)->id;
}

编辑

我在 boost 沙箱中找到了一个解决方案: sandbox Boost.Tokenmap

4

5 回答 5

2

我可以在构造函数调用之前设置成员变量吗?

不,但是您可以使用 ID 创建一个基类,在其构造函数中设置 ID(例如,如果无法分配 ID,则会引发异常)。从该类派生,并且在派生类进入构造函数的那一刻,ID 将已经设置。您还可以在另一个类中管理 id 生成 - 在某种全局单例中,或者您可以将 id manager 作为第一个参数传递给构造函数。

typedef int Id;
class IdObject{
public:
    Id getId() const{
        return id;
    }
protected:
    IdManager* getIdManager() ...
    IdObject()
    :id(0){
        IdManager* manager = getIdManager();
        id = manager->generateId();
        if (!id)
            throw IdException;
        manager->registerId(id, this);           
    }
    ~IdObject(){
        if (id)
            getIdManager()->unregisterId(id, this);
    }       
private:
    Id id;
    IdObject& operator=(IdObject &other){
    }
    IdObject(IdObject &other)
    :id(0){
    }
};

class DerivedObject: public IdObject{
public:
    DerivedObject(){
        //at this point, id is set.
    }
};

这种事。

于 2013-09-29T19:22:08.780 回答
1

如果您知道对象的未来地址(您的场景就是这种情况),那么是的,您可以做那种事情。但是,这不是明确定义的行为,因此很可能不是一个好主意(而且在每种情况下都不是好的设计)。尽管它可能会“正常工作”。

使用std::map上面评论中建议的 a 更干净,并且没有附加 UB 的“ifs”和“whens”。

尽管写入已知的内存地址可能会“正常工作”,但在构造函数运行之前对象并不存在,因此使用它的任何成员都是不好的魔力。
世事皆可能。没有编译器可能会做任何此类事情,但编译器可能会在运行构造函数之前将对象的存储设置为零,因此即使您不设置 ID 字段,它仍然会被覆盖。你无法知道,因为你在做什么是不确定的。

于 2013-09-29T18:44:26.903 回答
1

不,在调用其构造函数之前,您不能在对象中设置任何内容。但是,您有几个选择:

  1. 将 ID 传递给构造函数本身,因此它可以将 ID 存储在对象中。

  2. 在正在构造的对象前面分配额外的内存,将 ID 存储在该额外内存中,然后让对象在需要时访问该内存。

于 2013-09-29T18:27:47.740 回答
1

在构造函数调用之前,您是否有理由这样做?

从基于 ID 的池中分配对象如下:

1) allocate a free id from the pool
2) get a memory address based on the id value
3) construct the object on the memory address
4) set the ID member of the object and the deallocation is based on that id

根据您的步骤,您在构造函数之后设置 ID。

所以我想我在调用构造函数之前设置了 ID。

我不想直言不讳,但你需要有比这更好的理由来涉足未定义的行为领域。请记住,作为程序员,我们一直在学习很多东西,除非绝对没有办法绕过它,否则我们需要远离雷区,未定义的行为就是其中之一。

正如其他人指出的那样,是的,您可以做到,但这就像说您可以rm -rf /以 root 身份进行。并不意味着你应该:)

C 可以很容易地射中自己的脚。C++ 让它变得更难了,但是当你这样做时,你会毁掉你的整条腿!— Bjarne Stroustrup

于 2013-09-29T19:28:02.167 回答
1

是的,你可以做你正在做的事情,但这真的不是一个好主意。根据标准,您的代码调用Undefined Behavior

3.8 对象生命周期 [basic.life]

对象的生命周期是对象的运行时属性。如果一个对象是一个类或聚合类型,并且它或它的一个成员由一个普通默认构造函数以外的构造函数初始化,则称它具有非普通初始化。[注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化。— 尾注] T 类型对象的生命周期开始于:

获得了类型 T 的正确对齐和大小的存储,并且

如果对象有非平凡的初始化,它的初始化就完成了

T 类型对象的生命周期在以下情况下结束:

— 如果 T 是具有非平凡析构函数 (12.4) 的类类型,则析构函数调用开始,或者

——对象占用的存储空间被重用或释放。

在对象的生命周期开始之前但在对象将占用的存储空间分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前,指向该存储空间的任何指针可以使用对象将要或曾经位于的位置,但只能以有限的方式使用。对于正在建造或破坏的物体,见 12.7。否则,这样的指针指向已分配的存储空间 (3.7.4.2),并且像使用 void* 类型的指针一样使用该指针是明确定义的。这样的指针可能会被取消引用,但生成的左值只能以有限的方式使用,如下所述。如果出现以下情况,该程序具有未定义的行为:

指针用于访问非静态数据成员或调用对象的非静态成员函数

当您的代码调用 Undefined Behaviour 时,允许实现做它想做的任何事情。在大多数情况下,什么都不会发生——如果你幸运的话,你的编译器会警告你——但有时结果会出乎意料的灾难性。

您使用连续数组作为底层存储来描述由 N 个相同类型的对象组成的池。请注意,在这种情况下,您不需要为每个分配的对象存储一个整数 ID - 如果您有一个指向已分配对象的指针,您可以从数组中对象的偏移量派生 ID,如下所示:

struct Object
{
};

const int COUNT = 5; // allow enough storage for COUNT objects

char storage[sizeof(Object) * COUNT];

// interpret the storage as an array of Object
Object* pool = static_cast<Object*>(static_cast<void*>(storage));

Object* p = pool + 3; // get a pointer to the third slot in the pool
int id = p - pool; // find the ID '3' for the third slot
于 2013-09-29T19:01:06.797 回答