虚拟继承、默认构造函数和额外复制
Posted
技术标签:
【中文标题】虚拟继承、默认构造函数和额外复制【英文标题】:Virtual inheritance, default constructors and extra copying 【发布时间】:2015-03-21 11:55:05 【问题描述】:如果您查看下面的代码,您将看到有 A
、B
和 C
类,每个类都继承自上一个。继承是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& d)
调用func_b(Data& d)
,它调用func_c(Data& 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 只是简化了一个实际的菱形用例。
这正是我所做的。我删除了绒毛并选择不通过引用传递以更容易地说明问题,否则有十几个类都在交互,我会发布太多没有人愿意阅读的代码。以上是关于虚拟继承、默认构造函数和额外复制的主要内容,如果未能解决你的问题,请参考以下文章