成员变量和对象切片

Member variables and object slicing

This answer 建议可以通过使用指针来克服向量中的对象切片。根据我的测试,在处理基础和派生 classes 之间共享的变量时,情况确实如此。例如,给定这些 classes:

class Base {
public:
     int x = 1;
};

class Derived : public Base {
public:
    Derived() : Base() { 
        x = 2;
    }
    int y;
};

可见Derived在构造时重新定义了x为2。当放入 Base class 的向量中时,x 变量的行为符合预期。

vector<unique_ptr<Base>> baseVec;               // Vector of pointers to Base objects
unique_ptr<Derived> derivedPtr(new Derived());  // Pointer to Derived object
baseVec.push_back(std::move(derivedPtr));       // Add pointer with std::move()
std::cout << baseVec[0]->x << std::endl;        // Outputs 2

但是,尝试使用仅属于 Derived 的 y 变量会导致错误,C2039: 'y' is not a member of 'Base' 如下所示:

std::cout << baseVec[0]->y << std::endl;

有没有办法在保留派生 classes 独有的成员变量的同时绕过这个问题,如果没有,是否有更好的方法将对象存储在有序容器中?

编辑

之前编辑的相关内容已移至我下面的回答中。在找到使用智能指针的方法(或确保我的原始指针实现没有内存问题)之前,我不打算接受答案。

Is there a way to bypass this issue while preserving member variables unique to derivative classes, and if not is there a superior way to store objects in an ordered container?

有很多技巧可以实现类似的效果。所有这些都已在 SO 的不同答案中提到。
一份简短的(可能不完整的)简历:

  • 双重调度。完全基于多态性,基本思想是将访问者 class 传递给存储在容器中的实例,它们通过重载方法提升自己,将派生类型传回给访问者。

  • 类型擦除。您可以通过多种方式做到这一点。例如,您可以将派生的 classes 作为指向 void 或基 class 的指针以及指向接收擦除类型的函数的指针来存储。指向函数的指针应该是函数模板的特化地址,它能够将 class 静态转换回它的原始类型并使用它执行任何任务。

  • 标记联盟。您不直接存储派生的 classes。相反,您将它们包装在一个专用结构中,该结构携带足够的信息以通过一组标记(即匿名枚举)将它们转换回派生类型。

  • std::variant。从 C++17 开始,您可以使用 std::variant,这是一种类型安全的标记联合,您可以通过专门的访问者访问它。

  • AOB(任何其他业务)。

我不认为重复 SO 上已有的示例是值得的。您可以使用上面的关键字搜索它们并找到您要查找的内容。
每种技术都有其优点和缺点。其中一些允许您在调用者的上下文中提取原始类型,因此您可以在需要时将对象传递给其他函数。其他一些完全擦除类型并为您提供 handlers 以对原始类型执行操作,但您永远不会在调用者的上下文中取回它。由于所使用语言的特点和他们所吸引的演员,这些差异通常会影响性能(以一种您几乎无法观察到的方式)。

改变 classes

BaseDerived 的 class 定义应更改为:

class Base {
public:
    int x = 1;

    virtual void f() { }
};

class Derived : public Base {
public:
    Derived() : Base() { x = 2; }
    int y = 55;

    void f() { }
};

值得注意的新增内容是虚函数 m() 允许 dynamic_cast 处理指针转换。

使用unique_ptr

唯一指针可用于存储、检索、修改和安全删除指向 Derived 对象的指针,这些对象驻留在指向 Base 对象的指针向量中。下面的示例使用堆分配的对象。

/* Insertion */
// Create a vector of unique_ptr to Base and add a unique_ptr to Derived to it
std::vector<std::unique_ptr<Base>> v;
std::unique_ptr<Derived> p1(new Derived());
v.push_back(std::move(p1));

/* Retrieval */
// Release the pointer at base to place it into p2
std::unique_ptr<Derived> p2(dynamic_cast<Derived*>(v[0].release()));

/* Modification */
p2->x = 0xff;    // Modify x (Decimal 255)
p2->y = 0xffff;  // Modify y (Decimal 65535)
int y = p2->y;   // Copy the value of the Derived object's y to an int for use later

// Move the pointer to the modified object back to v
v[0] = std::move(p2); 

/* Output */
std::cout << v[0]->x << std::endl; // Outputs 255
std::cout << y << std::endl;       // OUtputs 65535

/* Delete */
// To safely delete the object, it must be treated as a pointer to a Derived object
// Thus it must be released from the control of v and deleted via p3, which is cast in the same manner as p2 was
std::unique_ptr<Derived> p3(dynamic_cast<Derived*>(v[0].release()));
p3.reset(); // This deletes the object as well, not just the reference to it!

按预期运行并输出 25565535