如何使复制构造函数也复制虚拟表?

How to make copy constructor copy also virtual table?

提问人:TarmoPikaro 提问时间:4/5/2023 最后编辑:TarmoPikaro 更新时间:4/6/2023 访问量:188

问:

下面是简单的代码片段。我想进行类继承,以便 的复制构造函数也可以初始化继承自 的派生类的就地虚拟表。I-A-BII

为简单起见,我们可以假设.sizeof(A) == sizeof(B) == sizeof(I)

#include <iostream>

using namespace std;

class B;

class I
{
public:
    virtual ~I()
    {
        cout << "~I()\n";
    }

    I()
    {
        cout << "I ctor\n";
    }

    virtual void print()
    {
        cout << "I::print()\n";
    }

    I(const I& clone);

};

class A : public I
{
public:
    ~A() override
    {
        cout << "~A()\n";
    }

    void print() override
    {
        cout << "A::print()\n";
    }
};

class B : public I
{
public:
    B()
    {
        cout << "B ctor\n";
    }

    B(const B& clone)
    {
        cout << "B copy ctor\n";
    }
    
    ~B() override
    {
        cout << "~B()\n";
    }

    void print() override
    {
        cout << "B::print()\n";
    }

};

I::I(const I& clone)
{
    cout << "B copy ctor 2\n";
    B* b = dynamic_cast<B*>((I*)&clone);
    if (b != nullptr)
    {
        this->~I();
        new (this) B(*b);
    }
}


class T
{
public:
    I GetA()
    {
        return B();
    }

};


int main()
{
    cout << "main() ->\n";
    T t;
    {
        cout << "- GetA()\n";
        I i = t.GetA();
        i.print();
        cout << "- before scope end\n";
    }
    cout << "- main scope\n";
    cout << "<- main()\n";
}

这将产生:

main() ->
- GetA()
I ctor
B ctor
B copy ctor 2
~I()
I ctor
B copy ctor
~B()
~I()
I::print()
- before scope end
~I()
- main scope
<- main()

但最后我希望看到被召唤,而不是.~B~I

main() ->
- GetA()
I ctor
B ctor
B copy ctor 2
~I()
I ctor
B copy ctor
~B()
~I()
B::print()            <--- changed
- before scope end
~B()                  <--- added
~I()
- main scope
<- main()

任何人都可以建议如何使用标准C++解决这个问题?

如果可能的话,我宁愿避免使用内存分配,但允许就地分配。目的是减少对无效指针/内存不足等问题的检查。newnew

C++ 继承 复制构造函数

评论

1赞 Drew Dormann 4/5/2023
我想你是说你希望你创建的有 'vtable 指针? 返回一个 .返回的不可能是秘密的类型。返回 an 将允许您想要的多态性。II i = t.GetA();BGetA()III&
1赞 TarmoPikaro 4/5/2023
如果使用 代替 ,则允许这样做。为什么这也不能在类实例上操作 - 我想该类的区别在于它的虚拟表,它存储在 .I*Ithis
1赞 Drew Dormann 4/5/2023
这是语言的规则。创建 type 的变量可以保证该变量具有 类型 。允许编译器知道这一点,并据此进行优化。II
1赞 dalfaB 4/5/2023
copy 构造函数必须创建副本。永远不要尝试做任何其他事情。对于编译器来说,它是一个副本,它总是可以使用 RVO 来优化它,从而避免您的定义。
1赞 Paul Sanders 4/5/2023
你可以在谷歌上搜索“克隆成语”,也许这就是你需要的。

答:

2赞 Paul Sanders 4/6/2023 #1

好吧,好吧,OP 已经询问了工作原理,并且快速的谷歌搜索并没有出现太多,所以这里有一个快速的纲要。clone idiom

让我们假设:

  1. 我们有一个具有一个或多个虚拟函数(纯函数或其他函数)的类。Base

  2. 派生自 的一个或多个类,其中部分或全部覆盖这些函数中的一个或多个。Base

  3. 当我们只有一个可用的指针时,我们需要构造其中一个对象的副本。Base

普通的旧副本构造函数在这里不会削减它,因此我们需要放置一些脚手架以提供等效的功能。通常的方法是向 添加一个虚拟函数,该函数通常是纯虚拟的(尽管它不一定是)。所以可能看起来像这样:cloneBaseBase

class Base
{
public:
    virtual ~Base () { }
    virtual std::unique_ptr <Base> clone () = 0;
    virtual const char *whoami () { return "base"; }
//  ...
};

然后,我们在继承自 的每个类中适当地实现。通常只需调用派生类的复制构造函数就足够了,例如:cloneBase

class Derived : public Base
{
public:
    virtual std::unique_ptr <Base> clone () override { return std::make_unique <Derived> (*this); }
    const char *whoami () override { return "derived"; }
//  ...
}

最后,一个小测试程序:

int main ()
{
    Derived d;
    Base *b = &d;
    auto b2 = b->clone ();
    std::cout << b2->whoami ();
}

打印(耶!这种方法的另一个好处是我们可以将参数传递给(我自己经常发现这很有用 - 我有时会传递一组标志位,详细说明要复制的内容)。derivedclone

现场演示

评论

0赞 TarmoPikaro 4/6/2023
这是可以理解的,但是例如,对于C++,您已经在处理两个变量(b和d)。在你超出范围后,你会遇到更多的问题(例如,b 指针可以比 d 实例更长寿)或处理内存管理问题,如果你想让它变得漂亮。(指针所有权、shared_ptr、unique_ptr、weak_ptr等)。使用 C# 非常简单,因为默认情况下 C# 不会复制类。我也发布了我自己的答案。(也不漂亮)。
0赞 n. m. could be an AI 4/6/2023
@TarmoPikaro 这是一个沼泽标准的克隆习语,几乎每个C++程序员都能理解。相比之下,你自己的代码是一团糟。
0赞 Paul Sanders 4/6/2023
@TarmoPikaro再看一遍。对象生存期问题可通过返回 .至于C#,它有垃圾回收功能,所以问题就消失了。clonestd:: unique_ptr
0赞 TarmoPikaro 4/8/2023
我认为就我而言,我使用的是标准 c++。所以混乱是 c++,而不是我的代码。(我可以使用 c++ 提供的任何语法)。我最初想避免打电话给新人。如果内存不足,New 可以返回 null 指针,并且需要检查该指针。检查成本很高,因为您需要编写和维护该代码。我正在与 C# 进行比较,因为在这种情况下它看起来比 c++ 更漂亮。
0赞 TarmoPikaro 4/6/2023 #2

接口不能直接使用,因为它是编译器将运行的类型。从理论上讲,可以手动将 vtable 替换为指针,但这不会发生在构造函数调用中,因为 vtable 在成功调用构造函数后已正确初始化。 替换也适用于虚拟方法,但不适用于普通类方法。II* (this)vtable

一种方法是具有单独的访问器类,该类将对 进行操作并将所有操作定向到 。(在下面的示例中称为 )。这可以通过自定义运算符 发生。I*I*IC->

为了避免对内存管理的调用 - 如 / 并处理潜在的内存不足分配失败和检查 - 一种方法是使用就地新构造 或者 - 然后需要分配足够的缓冲区来容纳 或 。newdeletenullptrnew (buffer) A|B()new (buffer) A|B(a|b)sizeof(A)sizeof(B)

在下面的示例中,如果缓冲区大小不够大,我将用于生成编译时错误。尝试将一些成员字段添加到或查看编译失败。分配更大的阵列,例如 来解决此问题。请注意,调试和发布版本的大小可能不同。static_assertABchar lbuf[20];

示例还显示了仅克隆基类时的零复制/构造/破坏 - 这是通过多个类共享同一指针来实现的。IC i2 = i;IC

完整示例如下:

#include <iostream>

using namespace std;

class B;

class I
{
public:
    virtual ~I()
    {
        cout << "~I()\n";
    }

    I()
    {
        cout << "I ctor\n";
    }

    virtual void print() = 0;

private:
    I(const I& clone) = delete;
};

class IC    //interface container
{
    // cannot be instantiated on it's own
    IC() = delete;
    // cannot instantiate from existing pointer (this class does not delete pi pointer)
    IC(I* pi) = delete;
public:
    // create new instance
    IC(int i);
    // copy ctor, reuse existing instance
    IC(const IC& ic);
    // create new instance from local copy
    IC(I&& i);

    I* operator->()
    {
        return pI.get();
    }

protected:
    std::shared_ptr<I> wrap(I* pi);
    std::shared_ptr<I> pI;
    char ibuf[sizeof(void*) /*hardcode different value as for max( sizeof(A), sizeof(B) ). For pure no-data classes this should be sufficient */];
};


class A : public I
{
public:
    A()
    {
        cout << "A ctor\n";
    }

    ~A() override
    {
        cout << "~A()\n";
    }

    A(const A& clone)
    {
        cout << "A copy ctor\n";
    }

    void print() override
    {
        cout << "A::print()\n";
    }
};

class B : public I
{
public:
    B()
    {
        cout << "B ctor\n";
    }

    B(const B& clone)
    {
        cout << "B copy ctor\n";
    }
    
    ~B() override
    {
        cout << "~B()\n";
    }

    void print() override
    {
        cout << "B::print()\n";
    }

};

std::shared_ptr<I> IC::wrap(I* pi)
{
    // we only destruct, but don't delete instance
    return shared_ptr<I>(pi, 
        [](I* pi)
        {
            pi->~I();
        }
    );
}


IC::IC(int i)
{
    static_assert(sizeof(ibuf) >= sizeof(A), "increase size of ibuf to hold A's value");
    static_assert(sizeof(ibuf) >= sizeof(B), "increase size of ibuf to hold B's value");
    switch (i)
    {
        case 0:
            pI = wrap(new (ibuf) A);
            break;
        default:
        case 1:
            pI = wrap(new (ibuf) B);
            break;
    }
}

IC::IC(const IC& ic)
{
    pI = ic.pI; // Keep reference on same object, don't copy or destruct anything
}

IC::IC(I&& i)
{
    if (B* b = dynamic_cast<B*>(&i))
    {
        pI = wrap(new (ibuf) B(*b));
    }
    else
    {
        if (A* a = dynamic_cast<A*>(&i))
        {
            pI = wrap(new (ibuf) A(*a));
        }
    }
}

class T
{
public:
    IC GetA()
    {
        return B();
    }

};


int main()
{
    cout << "main() ->\n";
    T t;
    {
        cout << "- GetA()\n";
        IC i = t.GetA();
        i->print();
        
        cout << "- clone object\n";
        IC i2 = i;
        i2->print();

        cout << "- before scope end\n";
    }
    cout << "- main scope\n";
    cout << "<- main()\n";
}

它的输出:

main() ->
- GetA()
I ctor
B ctor
I ctor
B copy ctor
~B()
~I()
B::print()
- clone object
B::print()
- before scope end
~B()
~I()
- main scope
<- main()

请注意,polymorphic_value-git 中也存在类似的概念,但需要单独使用标头。

评论

1赞 Michael Karcher 4/6/2023
这看起来像你正在重塑std::variant<A,B>
0赞 TarmoPikaro 4/6/2023
@MichaelKarcher您可以添加 std::variant<A,B> 的示例作为答案吗?根据这篇文章 cppstories.com/2020/04/variant-virtual-polymorphism.html - 这并不那么简单。
0赞 Michael Karcher 4/6/2023 #3

如果接口的实现集是已知的(如 https://stackoverflow.com/a/75946069 中要求的那样),则可以将繁重的工作委托给 。下面是用作后备存储的该代码的一个版本:std::variantstd::variant

#include <iostream>
#include <memory>
#include <variant>
#include <utility>

using namespace std;

class B;

class I
{
public:
    virtual ~I()
    {
        cout << "~I()\n";
    }

    I()
    {
        cout << "I ctor\n";
    }

    virtual void print() = 0;

private:
    I(const I& clone) = delete;
};

class A : public I
{
public:
    A()
    {
        cout << "A ctor\n";
    }

    ~A() override
    {
        cout << "~A()\n";
    }

    A(const A& clone)
    {
        cout << "A copy ctor\n";
    }

    void print() override
    {
        cout << "A::print()\n";
    }
};

class B : public I
{
public:
    B()
    {
        cout << "B ctor\n";
    }

    B(const B& clone)
    {
        cout << "B copy ctor\n";
    }
    
    B(B&& clone)
    {
        cout << "B move ctor\n";
    }
    
    ~B() override
    {
        cout << "~B()\n";
    }

    void print() override
    {
        cout << "B::print()\n";
    }

};

class IC    //interface container
{
    typedef std::variant<A,B> impl_t;
    impl_t impl;
public:
    // create new instance
    IC(int i);
    // copy existing instance
    IC(const impl_t& existing) : impl(existing) {}
    // move existing instance
    IC(impl_t && existing) : impl(std::move(existing)) {}

    I* operator->()
    {
        return std::visit([](auto & impl_) {return static_cast<I*>(&impl_);}, impl);
    }
};


IC::IC(int i) : impl(i == 0 ? impl_t(std::in_place_type_t<A>()) : 
                              impl_t(std::in_place_type_t<B>()))
{
}

class T
{
public:
    IC GetA1()
    {
        return IC(1);
    }

    IC GetA2()
    {
        return IC(B());
    }

};


int main()
{
    cout << "main() ->\n";
    T t;
    {
        cout << "- GetA1()\n";
        IC i = t.GetA1();
        i->print();
        
        cout << "- clone object\n";
        IC i2 = i;
        i2->print();

        cout << "- GetA2()\n";
        IC i3 = t.GetA2();
        i3->print();
        
        cout << "- before scope end\n";
    }
    cout << "- main scope\n";
    cout << "<- main()\n";
}

评论

0赞 TarmoPikaro 4/6/2023
哇。好!操作看起来有点丑陋,但可能是可行的解决方案。有没有关于?->in_place_type_t
0赞 Caleth 4/6/2023
@TarmoPikaro 是一个结构模板,它的存在是为了作为类似的东西的构造函数参数,请参阅此处的重载 5 和 6std::inplace_tstd::variant