如何保持在堆栈上分配的对象的动态类型

How to keep the dynamic type of an object allocated on the stack?

本文关键字:对象 动态 类型 分配 何保持 堆栈      更新时间:2023-10-16

假设我有:

class A { };
class B : public A { };
A f() { 
    if (Sunday())
        return A;
    else
        return B;
}

显然这不起作用,因为A的复制构造函数将被调用。是否有返回堆栈分配对象而不丢失它的类型?

我试过使用std::shared_ptr<A>,但它让我进入另一个问题,因为std::shared_ptr<B>不是std::shared_ptr<A>

不可能立即从创建该对象的函数中返回一个堆栈分配的(即本地)对象。局部对象在函数返回时销毁。您可以通过使用各种"智能指针"和类似的技术来隐藏/混淆对象分配的实际性质,但是对象本身应该动态地分配

除此之外,只要遵守本地对象生命周期规则,本地对象的多态性就会以与任何其他对象完全相同的方式工作。只需使用指针或引用

A a;
B b;
A *p = Sunday() ? &a : &b;
// Here `*p` is a polymorphic object

指针p在上面的例子中保持有效,只要本地对象存在,这意味着你不能从函数返回p

此外,正如您在上面的示例中看到的,它事先无条件地创建了两个对象,然后选择其中一个而不使用第二个对象。这不是很优雅。你不能在if语句的不同分支中创建这样的对象的不同版本,原因与你不能从函数多态地返回一个局部对象的原因完全相同:一旦创建对象的局部块完成,对象就被销毁。

后一个问题可以通过使用原始缓冲区和手动就地构造

来解决。
alignas(A) alignas(B) char object_buffer[1024]; 
// Assume it's big enough for A and B
A *p = Sunday() ? new(buffer) A() : new (buffer) B();
// Here `*p` is a polymorphic object
p->~A(); // Virtual destructor is required here

但它看起来不漂亮。类似的技术(涉及复制缓冲区)可能可以用于使局部多态对象在块边界下存活(参见@Dietmar k hl的回答)。

所以,再一次,如果你只想创建两个对象中的一个,并且让你的对象在块边界中存活,那么立即解决放置局部对象的问题是不可能的。您必须使用动态分配的对象。

由于切片,这是不可能的。使用std::unique_ptr代替。您不会丢失动态类型,但是只能通过A接口访问它。

最简单的方法当然是使用合适的智能指针,例如,std::unique_ptr<A>,作为返回类型,并在堆上分配对象:

std::unique_ptr<A> f() {
    return std::unique_ptr<A>(Sunday()? new B: new A);
}

对于返回可能指向Bstd::unique_ptr<A>的方法,A必须具有virtual析构函数,否则当std::unique_ptr<A>实际指向B对象时,代码可能导致未定义的行为。如果A没有virtual析构函数并且无法更改,则可以通过使用合适的std::shared_ptr<...>或使用合适的std::unique_ptr<...>:

delete来避免此问题。
std::unique_ptr<A, void(*)(A*)> f() {
    if (Sunday()) {
        return std::unique_ptr<A, void(*)(A*)>(new B, [](A* ptr){ delete static_cast<B*>(ptr); });
    }
    else {
        return std::unique_ptr<A, void(*)(A*)>(new A, [](A* ptr){ delete ptr; });
    }
}

如果你不想在堆上分配对象,你可以使用一个holder类型,它将unionAB存储在一起,然后适当地构造和析构(下面的代码假设AB的副本不会抛出异常;如有必要,可以添加合适的移动构造和移动赋值):

class holder {
    bool is_b;
    union {
        A a;
        B b;
    } element;
public:
    holder(): is_b(Sunday()) {
        if (this->is_b) {
            new(&this->element.b) B();
        }
        else {
            new(&this->element.a) A();
        }
    }
    holder(holder const& other) { this->copy(other); }
    void copy(holder const& other) {
        this->is_b = other.is_b;
        if (this->is_b) {
            new(&this->element.b) B(other.element.b);
        }
        else {
            new(&this->element.a) A(other.element.a);
        }
    }
    ~holder() { this->destroy(); }
    void destroy() {
        if (this->is_b) {
            this->element.b.~B();
        }
        else {
            this->element.a.~A();
        }
    }
    holder& operator= (holder const& other) {
        this->destroy();
        this->copy(other);
        return *this;
    }
    operator A const&() const { return this->is_b? this->element.b: this->element.a; }                
    operator A&()             { return this->is_b? this->element.b: this->element.a; }
};