一个带有指向其父对象的指针的对象是否应该定义一个复制构造函数?

5
如果一个对象A包含一个成员对象B,并且对象B有一个指向其父对象A的指针,那么我需要为对象B指定一个复制构造函数吗?
假设没有动态分配。
此外,在这里适用3个规则吗?

4
快快从这个设计中逃离。拥有一个指向父级对象的指针只意味着这个对象的设计很糟糕。 - SergeyA
3
@SergeyA:哎,其实也没那么糟糕。这只是通过政策的一般概括。虽然过于笼统,但也可以理解。 - Lightness Races in Orbit
1
@SergeyA:我所有的尝试都相当成功。 - Lightness Races in Orbit
3
实际上没有,我刚刚想起来我的单元测试套件是彻头彻尾的地狱,别提了。 - Lightness Races in Orbit
2
@SergeyA 会考虑双向链表吗? - flakes
显示剩余12条评论
3个回答

4
你的设计采用了双向导航的组合方式,这是完全有效的。但正如Sergey在评论中指出的那样,这种设计并非没有问题。
假设你有一个类Object和一个包含Object的类Container。以下是一些基本问题:
Container c;
Object mo1; // Q1: should this be valid ? (i.e. is an object without parent allowed 
Object mo2 = c.o;  // Q2: is this acceptable ?  Q3: Who is parent of mo2 ?  

看一下问题Q2和Q3:如果这样的初始化是可以接受的,那么立即出现的问题是你想要什么父级:
- 如果`mo2`不应该有父级,则需要根据3条规则编写复制构造函数以清除父级。 - 如果`mo2`应该引用相同的父级(尽管它不是成员),则可以保留默认的复制构造函数。
示例:
struct Container; 
struct Object{
    Container *parent;  
    Object (Container *p=nullptr) : parent(p) { cout << "construct object"<<endl; }
    // copy constructor or compiler generated one depending on Q2+Q3
    ~Object() {}
    ...
};
struct Container {
    Object o;
    Container() : o(this) {}
};

如果这样的初始化是不被接受的,你应该在代码中明确禁止复制构造。
Object (const Object &o) = delete;  

重要提示: 如果Object需要可复制,请注意Container也可能需要一个复制构造函数。无论您决定为Object做什么,如果要复制,您可能需要在Container中处理它。在Container中绝不能使用Object的复制构造函数。

重要提示2: Container本身可能在更复杂的情况下被使用。以vector<Container>为例。当向向量添加新容器时,可能会执行重新定位操作,如果您没有提供一个能够处理的Container复制构造函数,则可能会使指针无效!


2
谢谢 - 像这样的东西让我想完全放弃编程! - tom1989

0

是的,即使子对象不管理任何句柄或指向资源的指针(这将导致没有此类构造函数的副本盲目共享并导致熟悉的问题),您也应考虑实现复制构造函数。

原因是父级的子项的副本不一定被认为是该父级的子项!

如果将子项的副本视为属于同一父项,并且父项有一个子项列表,则谁的责任是将其插入该列表中?必须在某个地方完成。如果未在复制构造函数中完成,则代码中执行复制的每个位置都必须记住执行一些其他操作,例如copy.register_with_parent()。任何忘记此操作的地方很可能都是错误。

如果子项的副本不被认为属于同一父项,则天真的成员逐一复制仍将具有父指针。一个对象持有指向并非其实际父项的父指针是什么意思?必须清除该指针或将其设置为“所有孤儿子对象的默认父项”或其他内容。这将在复制构造函数中完成。

如果您不知道这些问题的答案,那么定义一个未实现的复制构造函数,以便在您知道行为应该是什么之前不进行复制。因此,再次声明但未定义复制构造函数!

-1

哦,但是父对象不能拥有其派生类对象,除非使用指针分配:

class B;

class A
{
    B son;
};

class B : public A
{
    A* parent;
};

结果:

field 'son' has incomplete type 'B'
     B son;
       ^

然后,如果您使用分配完成它:

class B;

class A
{
public:
    A(void);
    ~A();
private:
    B* son;
};

class B : public A
{
public:
    B() {} // <-depends what you want to archieve
private:
    A* parent;
};

A::A(void) : son(new B) {}
A::~A() {delete son;}

此时会出现问题。您想让父指针指向全局对象A,分配自己的A对象,还是指向this
全局变量-无需编写复制构造函数:
// A class not changed
A global;
class B : public A
{
public:
    B() {parent = &global;}
...

如果对象分配了自己的A对象,则需要遵循3(或4)条规则。此外,您的类A也需要遵循此规则,因为我们将复制父对象,因此每个B对象都有自己的对象:

class B;

class A
{
public:
    A(void);
    A(const A& other);
    A& operator=(const A& other);
    ~A();
private:
    B* son;
};

class B : public A
{
public:
    B() : parent(new A) {}
    B(const B& other) : parent(new A(*other.parent)) {}
    B& operator=(const B& other);
    ~B() {delete parent;}
private:
    A* parent;
};

A::A(void) : son(new B) {}
A::A(const A& other) : son(new B(*other.son)) {}
A& A::operator=(const A& other)
{
    delete son;
    son = new B(*other.son);
    return *this;
}

B& B::operator=(const B& other)
{
    delete parent;
    parent = new A(*other.parent);
    return *this;
}

A::~A() {delete son;}

最后,对象B指向相同的对象基类,不需要像全局变量那样特殊处理:
class B;

class A
{
public:
    A(void);
    ~A();
private:
    B* son;
};

class B : public A
{
public:
    B() : parent(this) {} // pointless, but yeah, nothing else is needed.
private:
    A* parent;
};

A::A(void) : son(new B) {}
A::~A() {delete son;}

这不是关于什么应该做和什么不应该做的问题。一切都取决于你想要实现什么。


他们所说的父/子是指“包含”,而不是“继承”。如果你建议B应该从A继承,并且还持有一个指向A的指针,那么这两个A之间有什么区别呢?我希望它们不是同一个对象!无论如何,通常人们会以相反的方式进行,即在父类之前完全声明子类,然后将其作为父类的成员组合,而子类只能获得对(提前声明的)父类的引用/指针。 - underscore_d
@underscore_d 这就是我的问题!嗯,你说得对,但是A和B这种表示法在类继承中被广泛使用,所以我就掉进了这个陷阱。 - xinaiz
我认为普遍共识是父/子描述组合,而继承描述为基/派生。抱歉,尽管可能与OP无关,但这些人为的例子对于任何实际情况有用吗?你说“不是关于应该做什么和不应该做什么”,但随后建议使用全局变量和裸指针,这两个东西在99%的情况下都是不被建议使用或不适合的! - underscore_d
我知道这样做是不应该的,而且我从来不这样做,但由于问题不清楚,所以问题包含了这些特性。如果你忠实地遵循标准库,使用普通类甚至不需要遵守三个规则中的任何一个。 - xinaiz
@underscore_d - 是的,我应该表达得更清楚。我的意思是包含而不是继承。 - tom1989

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接