C++中的循环依赖问题

19

事实:

  • 我有两个主要的类别:Manager和Specialist。
  • 有几种不同类型的Specialist。
  • Specialist经常需要其他Specialist的帮助来完成工作。
  • Manager知道所有的Specialist,而每个Specialist最初只知道他们的Manager。(这是问题所在。)
  • 在运行时,Manager创建并存储Specialist列表。然后Manager遍历列表并要求每个Specialist进行初始化。在它们的初始化期间,每个Specialist要求Manager提供满足某些描述的其他Specialist。完成此操作后,Manager进入循环,在此期间按顺序要求Specialists执行其专门任务。

对我来说,这似乎是一个不错的模式,但由于Manager具有Specialist列表,而Specialist具有Manager,我遇到了循环依赖问题。

这是一种应该如何从另一个类中声明存在的情况吗?(如果是这样,怎么做?)还是应该使用一些设计模式来解决这个问题?(如果是这样,应该用什么?)另外......我认为这个模式本身还可以,所以我不介意有人帮我理解为什么这是个坏事。


你能给我们展示一下你所拥有的样本,以及具体遇到了什么问题吗? - David Thornley
我最近看到了很多类似的问题 - 这是最近的一个:http://stackoverflow.com/questions/4016471/c-circular-reference-problem - Greg Domjan
@Greg - 这些问题很相似,但是我不仅对解决循环依赖感兴趣,还想了解我使用的模式是否有缺陷。 - JnBrymn
你的情况在设计模式中非常普遍,检查观察者设计模式UML。你需要前向声明,所以也要写上它... - RoundPi
5个回答

26
在两种情况下,都需要前置声明其他类: Manager.h
class Specialist;

class Manager
{
    std::list<Specialist*> m_specialists;
};

Specialist.h

class Manager;

class Specialist
{
    Manager* m_myManager;
};

只有在需要使用类的成员函数或变量,或需要将类用作值类型等时,才需要引入该类的头文件。当您只需要指向类的指针或引用时,前向声明就足够了。

请注意,前向声明不仅用于解决循环依赖关系。应尽可能使用前向声明。如果可行,它们始终优于包含额外的头文件。


5
我非常不同意"They are always preferable"这个说法,我认为只要能够避免使用前置声明,它们就是很少被优先选用的。在大型项目中使用前置声明会导致代码更难理解,因为这使得跟踪依赖项变得更加困难。此外,现代编译器可以缓存文件并支持预编译头,因此通常几乎没有性能提升。【关于另一个无关的问题,您为什么建议使用std::list?】 - James McNellis
3
追踪哪些依赖关系?如果使用前置声明有效,则不再存在依赖关系。此外,在任何一种半好的 IDE 中,跟踪类路径与头文件路径同样容易。至于使用 std::list,OP 中提到 Manager 存储了一份专家列表,所以我决定字面理解。我自己可能会使用 vector,但这当然取决于特定的用例。 - Peter Alexander
谢谢!我担心我正在输入某种非直观的反模式。很高兴看到每个人都认为这是相当标准的做法。 - JnBrymn

13

这是一个审美问题,但即使没有循环依赖,前向声明通常也是头文件中包含的良好替代品(我不想在这里引发讨论)。因此,这里提供了一个示例,说明如何为您的问题应用前向声明:

在Manager.h中:

// Forward declaration:
class Specialist;

// Class declaration:
class Manager
{
    // Manager declarations go here.
    // Only pointers or references to
    // the Specialist class are used.
};

在Manager.cpp文件中:

#include "Manager.h"

#include "Specialist.h"

// Manager definitions/implementations
// using the Specialist class go here.
// Full Specialist functionality can be used.

在 Specialist.h 文件中:

// Forward declaration:
class Manager;

// Class declaration:
class Specialist
{
    // Specialist declarations go here.
    // Only pointers or references to
    // the Manager class are used.
};

在 Specialist.cpp 文件中:

#include "Specialist.h"

#include "Manager.h"

// Specialist definitions/implementations
// using the Manager class go here.
// Full Manager functionality can be used.

谢谢你提供这个详细的例子,这是到目前为止唯一有用的答案! - fuenfundachtzig
但是当没有包含Manager.h文件时,你如何在Manager.cpp中实现这些函数呢? - akuzminykh
当然,Manager.h也包含在Manager.cpp中。我故意避免解释这个暗示。如果这不清楚或可能会导致混淆,我现在已经编辑了我的答案。 - Flinsch

1
一个选项是像你建议的那样提前声明其中一个人员:
struct specialist;

struct manager
{
    std::vector<std::shared_ptr<specialist> > subordinates_;
};

struct specialist
{
    std::weak_ptr<manager> boss_;
};

然而,如果你最终拥有更多的树形结构(其中有多个管理层),那么一个person基类也可以起到作用:

struct person
{
    virtual ~person() { }
    std::weak_ptr<person> boss_;
    std::vector<std::shared_ptr<person> > subordinates_;
};

你可以根据层次结构为不同类型的人派生特定的类。是否需要这样做取决于你打算如何使用这些类。

如果你的实现不支持std::shared_ptr,它可能支持std::tr1::shared_ptr或者你可以使用boost::shared_ptr


在这个模型中,您必须确保所有的manager指针都被包装在shared_ptr中吗?否则就没有办法验证weak_ptr是否存在了?我很好奇这一点,因为对我来说经常遇到这种情况。 - Steve Townsend
@Steve:是的。如果层次结构中只有一个类类型(例如我第二个示例中的“person”),那么这将更容易。 - James McNellis

1

这只是普通的东西。你只需要

class Manager;

在专业头部中

class Specialist; 

在管理器头部

如果您正在使用shared_ptrs,您可能会发现shared_from_this很有用。(不是为了循环,而是因为它听起来您无论如何都需要它)


1

当其他人回答核心问题时,我想指出以下内容。

在运行时,管理器创建并存储专家列表。然后,管理器遍历列表并要求每个专家进行初始化。在它们的初始化期间,每个专家都要求管理器提供满足某些描述的其他专家。完成此操作后,管理器进入循环,在此期间按顺序要求专家执行其专业任务。

我只想指出这需要一个两步骤的过程。如果管理器只知道一个专家,如何告诉专家1存在哪些专家可以执行任务B?所以你需要:

1)管理器遍历专家列表并要求他们进行身份识别。

2)管理器遍历专家列表并要求他们访问他们需要访问的专业知识,告诉他们谁可以满足他们的要求。

3)管理器遍历专家列表并告诉他们执行操作。


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