虚拟继承、默认构造函数和额外复制

Posted

技术标签:

【中文标题】虚拟继承、默认构造函数和额外复制【英文标题】:Virtual inheritance, default constructors and extra copying 【发布时间】:2015-03-21 11:55:05 【问题描述】:

如果您查看下面的代码,您将看到有 ABC 类,每个类都继承自上一个。继承是virtual,这意味着C必须调用A的构造函数,即使它不直接继承自A

这一切都很好,但是有两种方法可以处理这个问题,而且它们都对我来说似乎很老套,所以我想知道这是否真的是你应该这样做的。

第一种方式(下面的代码)似乎调用了两次A::A(a),一次在B 的构造函数中,然后再次在C 的构造函数中。当然它不会被构造两次,但这样做的副作用是参数a 被复制了额外的时间,这样它就可以传递给B::B(a, b),即使该函数最终没有使用价值。它只需要构造A::A(a)的值,但是这里的构造函数是从C调用的,所以传递给B::B(a, b)a的副本被简单地丢弃了。

为了避免这种浪费的副本,另一种方法是在基类中创建额外的构造函数(取消注释下面的代码为此选项。)这避免了浪费的副本,但是缺点是您作为程序员正在创建一个全新的您的类的构造函数,它实际上从未在运行时被代码调用。您必须声明该函数,实现它(即使它是空的)并实际编写调用它的代码,即使该代码永远不会真正运行!这种事情通常表明某处的设计有问题,所以我想知道是否有另一种方法可以实现这一点,而不是那么狡猾。

#include <iostream>

struct Data 
    Data() 
        std::cout << "Creating data\n";
    
    Data(const Data& d) 
        std::cout << "Copying data\n";
    
;

struct A 
    A(Data a)
    
        std::cout << "A(a)\n";
    
/*
    A()
    
        std::cout << "A()\n";
    
*/
;

struct B: virtual public A 
    B(Data a, Data b)
        :   A(a)
    
        std::cout << "B(a, b)\n";
    
/*
    B(Data b)
    
        std::cout << "B(b)\n";
    
*/
;

struct C: virtual public B 
    C(Data a, Data b, Data c)
        :   A(a),
            B(a, b)
/*
            B(b)
*/
    
        std::cout << "C(a, b, c)\n";
    
;

int main(void)

    Data a, b, c;
    std::cout << "-- B --\n";
    B bb(a, b);
    std::cout << "-- C --\n";
    C cc(a, b, c);

    return 0;

编辑:为了澄清,有几个人发表了评论说我应该通过引用传递。忽略这并不总是你想要的事实(想想智能指针),它并没有消除你将一个值传递给一个永远不会被使用的函数的事实,这看起来很狡猾。所以选择要么传递一个被默默丢弃的值(希望你永远不会被它咬伤),要么实现一个你永远不会调用的函数(希望也永远不会回来咬人。)

所以我最初的问题仍然存在 - 这些真的是唯一的两种方法吗?您必须在多余的变量或多余的函数之间做出选择?

【问题讨论】:

只通过引用传递参数。 你好 Malvineous,我想知道你为什么不接受 Mateusz 的回答? @niceman 并不是每个人都一直在现场巡逻。 OP 有 3k 个代表和 62 个问题,其中大多数都有一个公认的答案。你可以假设他们知道如何/何时接受。 @niceman 我想我没有遇到 OP 想要解决的真正问题。 【参考方案1】:

我不确定,如果这是你的意图,但你做了很多不必要的 Data 副本,因为每个参数都是按值传递的。这意味着,每次调用A::A(a) 时,都会创建Data 的新实例,它是从a 参数复制构造的。在B::B(a,b) 中制作了两个这样的副本,在C::C(a,b,c) 中制作了三个副本。但请注意,B::B(a,b) 调用 A::A(a)C::C(a,b,c) 调用 B::B(a,b),所以我猜,调用 C::C(a,b,c) 实际上会创建 六个 Data 的副本:

三个副本a 两份 b 一个 c的副本

您可以通过引用传递构造函数的参数来避免这种情况 - 然后,不会复制,除非您在构造函数主体或初始化列表中决定复制。

试试这个:

#include <iostream>

struct Data 
    Data() 
        std::cout << "Creating data\n";
    
    Data(const Data& d) 
        std::cout << "Copying data\n";
    
;

struct A 
    A(const Data& a)
    
        std::cout << "A(a)\n";
    
/*
    A()
    
        std::cout << "A()\n";
    
*/
;

struct B: virtual public A 
    B(const Data& a, const Data& b)
        :   A(a)
    
        std::cout << "B(a, b)\n";
    
/*
    B(Data b)
    
        std::cout << "B(b)\n";
    
*/
;

struct C: virtual public B 
    C(const Data& a, const Data& b, const Data& c)
        :   A(a),
            B(a, b)
/*
            B(b)
*/
    
        std::cout << "C(a, b, c)\n";
    
;

int main(void)

    Data a, b, c;
    std::cout << "-- B --\n";
    B bb(a, b);
    std::cout << "-- C --\n";
    C cc(a, b, c);

    return 0;

输出:

Creating data
Creating data
Creating data
-- B --
A(a)
B(a, b)
-- C --
A(a)
B(a, b)
C(a, b, c)

编辑:

我明白了,我误解了你的问题。现在,我不确定我是否完全理解它。编译原始代码后:

[输出]

[Cut]
-- C --
Copying data // copy of c in C::C()
Copying data // copy of b in C::C()
Copying data // copy of a in C::C()
Copying data // copy of a passed to A::A()
A(a)
Copying data // copy of b passed to B::B()
Copying data // copy of a passed to B::B()
B(a, b)
C(a, b, c)

现在,您要删除哪些副本?

您写道,A::A() 被调用了两次 - 在 B::B()C::C() 中。但是您创建了两个对象,一个是B 类型,一个是C 类型。这两种类型都继承自A,因此A::A() 将始终被调用——无论是否显式。如果您不显式调用A::A(),则将调用A 的默认构造函数。如果未定义,将产生编译器错误(尝试从C::C() 中删除对B(a, b) 的调用)。

【讨论】:

抱歉,我的问题可能不清楚。我没有故意通过引用来表明副本正在发生。我已经相应地编辑了这个问题。即使您像在代码中所做的那样将其更改为使用引用,您仍然在 C 构造函数中调用 B(a, b),但 a 的值永远不会被使用和丢弃。如果它是 std::unique_ptr 怎么办 - 你不能将它传递给两个不同的函数! 抱歉,我的意思有点难以解释。我要删除的副本是copy of a passed to B::B(),但问题不一定是副本本身,副本是潜在问题的表现 - 即a 被传递给A::A()B::B() 而实际上只有其中一个构造函数需要它。但是我必须将它传递给两个构造函数,而一个简单地忽略它。对我来说,这看起来像是糟糕的设计,这让我觉得我做错了什么。 但这不是一个糟糕的设计,C++ 就是这样工作的。这就是为什么我们首先关注值传递——如果你需要传递一个如此“深”的对象,你不能通过值来做到这一点。想象一下,你有一组函数调用:func_a(Data&amp; d)调用func_b(Data&amp; d),它调用func_c(Data&amp; d)...等等,假设有8个调用级别,每个调用都传递对象,它作为参数接收.现在,更改这些函数的原型,使它们按值接受d。这是一个糟糕的设计。你明白吗?您有责任确保您的自定义类型的某些语义是有效的, [...] 定义明确,不会对性能产生重大影响。你举了一个std::unique_ptr 的例子——这个类没有定义复制构造函数,所以你总是必须通过引用传递它。如果是std::shared_ptr - 那又怎样?我们会有一个额外的副本,它会在构造函数退出后立即销毁。这个副本的成本是微不足道的(参考计数器的简单递增/递减),所以没有痛苦。您将始终以一种或另一种方式为一切付出代价。只要确保价格不会太高,当收益不值得时。 这很公平,如果必须如此,那就这样吧。但是,如果您暂时忘记了副本,您仍然必须传递一个未使用的参数。即使您通过引用传递std::unique_ptr,您会将它传递给哪个函数? A::A 还是 B::B?您必须查看 B::B 的实现,并查看它没有使用其 unique_ptr 参数,然后将 nullptr 传递给那里。必须知道实现内部发生了什么似乎违背了关于封装的常识。当然,如果是这样的话,那就这样吧,但我只是问,因为它看起来很可疑。【参考方案2】:

您使用虚拟继承的方式是错误的,在这个问题中 - “额外副本”和虚拟继承之间没有关系

虚拟继承是为了解决 Diamond 问题 - 如果 Horse 和 Bird 都继承自 "Animal" ,而 Pegasus 继承自 Horse 和 Bird - 我们不希望包含 "Animal" 中的每个成员变量两次。因此我们实际上继承了 Horse 和 Bird,以便在 Pegasus 中只有一次 Animal Object。

这里,这个例子没有提出菱形问题,因此没有反映虚拟继承的任何实际使用(否则它会强制首先调用 A ctor。)

另外,有这么多副本的原因是因为您通过值传递所有内容,而不是您应该的 const 引用。将所有内容作为引用传递将避开这里的大部分副本。

【讨论】:

前两段是错的。 OP使用虚拟继承,因此有虚拟继承,句号。是的,VI 通常用于防止菱形问题,但继承是否为虚拟仅取决于所使用的 virtual 关键字。钻石或无钻石对继承虚拟性没有影响。 当然编译器会将所有内容编译为虚拟继承。关键是从概念上讲,“额外副本”与继承是虚拟的事实之间没有关系,而且,我试着提到这个例子不使用虚拟继承,因为它应该被使用 我稍微改了一下答案 我会说这没有错,只是没必要。但也许 OP 只是简化了一个实际的菱形用例。 这正是我所做的。我删除了绒毛并选择不通过引用传递以更容易地说明问题,否则有十几个类都在交互,我会发布太多没有人愿意阅读的代码。

以上是关于虚拟继承、默认构造函数和额外复制的主要内容,如果未能解决你的问题,请参考以下文章

使用虚拟继承时调用默认构造函数[重复]

如何在不破坏移动和复制构造函数的情况下声明虚拟析构函数

拷贝构造函数 和 赋值操作符重载

使用虚拟继承和委托构造函数在构造函数中崩溃

C++在单继承多继承虚继承时,构造函数复制构造函数赋值操作符析构函数的执行顺序和执行内容

继承默认构造函数,矛盾吗?