【问题标题】:Can I set a member variable before constructor call?我可以在构造函数调用之前设置成员变量吗?
【发布时间】:2013-09-29 18:17:24
【问题描述】:

我开始实现一个基于 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

【问题讨论】:

  • 代码很多;您能指出与您的问题相关的部分吗?
  • 我在代码中放了两条注释,也许我应该以某种方式标记它
  • 我建议你删除所有与问题不直接相关的代码,并构造一个 minimal test-case 来演示你在说什么;)
  • 如果你问你是否可以做this,那么答案是否定的(即使它有效:))。
  • @Industrial-antidepressant:新代码使您的问题更加清晰 100 倍。谢谢!

标签: c++ memory-management c++11


【解决方案1】:

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

不,但您可以创建一个带有 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.
    }
};

这种东西。

【讨论】:

  • 所以我使用 ID 进行内存分配/释放。因此,我根据 ID 从池中获取 DerivedObject,ID 是 std::vector. 中的索引(与 sizeof(DerivedObject) 相乘后)
  • @Industrial-antidepressant:这是一个问题还是一个评论?到目前为止,我没有发现任何问题。
  • 这太复杂了。甚至不需要存储 ID - 它可以从对象在底层存储中的偏移量派生出来。
  • @willj:他要id,我告诉他怎么加id。当然,首先分配内存,然后分配 ID 更简单,但那是另一回事了。 “它可以派生”仅当您有一个无法增长的池时。如果您的池必须增长,您将无法重新分配整个池(至少在 C++03 中),因为它会破坏所有已分配的对象。所以你最终会分块分配它。使用块,您无法计算偏移量。
  • 请问您可以 s/Manager/Registry/ 吗?
【解决方案2】:

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

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

对象的生命周期是对象的运行时属性。 说一个对象有非平凡的初始化 如果它是类或聚合类型,并且它或其成员之一由构造函数初始化,而不是琐碎的 默认构造函数。 [注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化。 — end note ] 类型 T 的对象的生命周期开始于:

——获得了适合类型 T 的对齐方式和大小的存储,并且

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

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

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

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

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

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

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

您描述了一个包含 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

【讨论】:

  • 真的吗?我不访问该成员,而是设置它并在对象生命周期内访问它。或不?或者在构造函数调用之前取消内存(例如calloc)也是UD?
  • 在标准术语中,“访问”是指使用.-&gt; 语法涉及类成员访问的任何内容。 callocmemset 等函数不执行类成员访问,在这种情况下,此规则不适用。
【解决方案3】:

不,在对象的构造函数被调用之前,你不能在对象中设置任何东西。但是,您有几个选择:

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

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

【讨论】:

  • 但是为什么呢?在构造函数中,我没有触摸“ID”成员。
  • 但是对象在其生命周期内是否需要知道其 ID?如果是,那么在构造函数中传递 ID 是有意义的。即使构造函数本身不使用 ID,它仍然可以初始化 ID 以供使用。但是如果对象不需要知道ID,那么在对象中存储ID就没有意义了。
【解决方案4】:

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

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

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

【讨论】:

  • 那么构造函数可以在调用之前修改对象的内存区域吗?这符合标准吗?
  • 不,构造函数会在其大括号内的代码运行之前立即修改对象的内存(如果您指定了初始化列表),或者 在它运行时(构造函数你写的代码)。然而,如果你知道地址,you 仍然可以在构造函数运行之前修改对象的内存——它只是不是定义的行为,所以从技术上讲,它是“错误的”做。一切皆有可能,它可能有效,也可能无效。标准中没有任何内容禁止编译器做其他事情(想想调试构建,例如用特殊模式填充内存)。
【解决方案5】:

在构造函数调用之前你有什么理由要这样做吗?

从基于 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。

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

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

C 让你很容易在脚上开枪。 C++ 让它变得更难了,但是当你这样做时,你会毁掉你的整条腿! — Bjarne Stroustrup

【讨论】:

  • 是的,我想在对象的构造函数中打印出ID。
  • 所以将ID作为构造函数参数传入。它甚至不需要是成员变量。为什么一个类想知道它被分配到哪里的细节?
猜你喜欢
  • 2012-09-28
  • 1970-01-01
  • 1970-01-01
  • 2013-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-08
相关资源
最近更新 更多