使用引用作为类成员来管理依赖关系

36
在使用一些内存管理语言后,我决定回到C++,但是我对于实现依赖注入的最佳方式感到迷茫。(我完全认同DI,因为我发现它是使TDD变得非常简单的最简单方法)。现在,浏览SO和Google给了我很多意见,让我有些困惑。在回答这个问题Dependency injection in C ++时,有人建议即使进行依赖注入也不应该传递原始指针,我理解这与对象所有权有关。现在,对象所有权也被处理了(虽然没有足够详细的状态;))在臭名昭著的Google样式指南中:http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Smart_Pointers 。所以我理解的是,为了使哪个对象拥有哪些其他对象更清晰,您应该避免传递原始指针。特别是,它似乎反对这种编码方式:
class Addict {
   // Something I depend on (hence, the Addict name. sorry.)
   Dependency * dependency_;
public:
   Addict(Dependency * dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_->do_something();
   }
   // ... whatever ... 
};    

如果Dependency是一个纯虚类(也称为穷人版接口),那么这段代码可以轻松地注入一个模拟版本的Dependency(使用类似google mock的东西)。
问题在于,我真的看不出这种代码可能会遇到什么麻烦,以及为什么我应该想要使用除了原始指针之外的任何东西!难道不清楚依赖关系来自哪里吗?
此外,我阅读了很多帖子暗示在这种情况下确实应该使用引用,那么这种代码更好吗?
class Addict {
   // Something I depend on (hence, the Addict name. sorry.)
   const Dependency & dependency_;
  public:
   Addict(const Dependency & dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_.do_something();
   }
   // ... whatever ... 
};

我得到了其他同等权威的建议反对使用引用作为成员变量:http://billharlan.com/pub/papers/Managing_Cpp_Objects.html 正如您所看到的,我并不确定各种方法的相对优缺点,所以我有点困惑。如果这已经被讨论过了或者只是在给定项目内个人选择和一致性的问题...但任何想法都是受欢迎的。

答案总结

(我不知道这是否符合良好的SO礼仪,但我会添加代码示例,以展示我从答案中收集到的内容...)

从各种回复中,以下是我可能会在我的情况下采取的做法:

  • 将依赖项作为引用传递(至少确保NULL不可能)
  • 在一般情况下,如果不能进行复制,则明确禁止复制,并将依赖项存储为引用
  • 在较罕见的情况下,如果可以进行复制,则将依赖项存储为原始指针
  • 让依赖项的创建者(某种工厂)决定堆栈分配或动态分配(在后一种情况下,通过智能指针进行管理)
  • 建立一个约定来将依赖项与自己的资源分开

因此,我最终会得到类似于以下内容:

class NonCopyableAddict {
    Dependency & dep_dependency_;

    // Prevent copying
    NonCopyableAddict & operator = (const NonCopyableAddict & other) {}
    NonCopyableAddict(const NonCopyableAddict & other) {}

public:
    NonCopyableAddict(Dependency & dependency) : dep_dependency_(dep_dependency) {
    }
    ~NonCopyableAddict() {
      // No risk to try and delete the reference to dep_dependency_ ;)
    }
    //...
    void do_some_stuff() {
      dep_dependency_.some_function();
    }
};

而对于可复制的类:

class CopyableAddict {
    Dependency * dep_dependency_;

public: 
    // Prevent copying
    CopyableAddict & operator = (const CopyableAddict & other) {
       // Do whatever makes sense ... or let the default operator work ? 
    }
    CopyableAddict(const CopyableAddict & other) {
       // Do whatever makes sense ...
    }


    CopyableAddict(Dependency & dependency) : dep_dependency_(&dep_dependency) {
    }
    ~CopyableAddict() {
      // You might be tempted to delete the pointer, but its name starts with dep_, 
      // so by convention you know it is not your job
    }
    //...
    void do_some_stuff() {
      dep_dependency_->some_function();
    }
};

据我所了解,没有办法表达“我有一些东西的指针,但我不拥有它”的意图,编译器也无法强制执行。因此,我必须在这里采用命名约定...

保存作为参考

正如Martin指出的那样,以下示例并没有解决问题。

或者,假设我有一个拷贝构造函数,可以这样实现:

class Addict {
   Dependency dependency_;
  public:
   Addict(const Dependency & dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_.do_something();
   }
   // ... whatever ... 
};

2
成员函数和友元函数仍然可以调用您的私有函数(复制和赋值运算符)。因此,要真正防止人们调用复制或赋值运算符,只需将其声明为私有而不提供定义即可。例如:NonCopyableAddict&operator =(const NonCopyableAddict&other); - clickMe
我真的很喜欢你的总结,我会建议一个小改进来避免像"未使用的变量"这样的错误,尤其是如果你正在执行"零警告"政策:// 防止复制 ** NonCopyableAddict & operator = (const NonCopyableAddict&); NonCopyableAddict(const NonCopyableAddict& ); ** - Andrei Bacescu
我真的很喜欢你的总结,我会建议一个小改进,以避免出现“未使用变量”等错误,特别是如果你实施了“零警告”政策:// 防止复制 ** NonCopyableAddict & operator = (const NonCopyableAddict&); NonCopyableAddict(const NonCopyableAddict&); ** - undefined
7个回答

13

没有硬性规定:
正如人们所提到的,在对象内部使用引用可能会导致复制问题(确实会),因此它不是一种万能解决方案,但对于某些情况它可能有用(这就是为什么C++给我们提供了以这些不同方式进行操作的选项)。但是,使用裸指针真的不是一个选择。如果您正在动态分配对象,则应始终使用智能指针来维护它们,并且您的对象也应该使用智能指针。

对于那些需要示例的人:流总是作为引用传递和存储的(因为它们不能被复制)。

关于您的代码示例的一些评论:

示例一和示例二

您使用指针的第一个示例与使用引用的第二个示例基本相同。区别在于引用不能为NULL。当您传递引用时,对象已经存在并且应该具有比您要测试的对象更长的寿命(如果它是在堆栈上创建的),因此保留引用应该是安全的。如果您动态创建依赖项指针,则应考虑使用boost::shared_pointer或std::auto_ptr,这取决于依赖项的所有权是否共享。

示例三:

我没有看到您第三个示例的任何巨大用途。这是因为您不能使用多态类型(如果传递从Dependency派生的对象,则会在复制操作期间进行切片)。因此,代码可能与Addict中的代码一样位于单独的类中。

Bill Harlen: (http://billharlan.com/pub/papers/Managing%5FCpp%5FObjects.html)

不是要贬低Bill,但:

  1. 我从未听说过他。
    • 他是地球物理学家而不是计算机程序员
    • 他建议使用Java来改善您的C++
    • 这两种语言现在在用法上完全不同,这是完全错误的)。
    • 如果您想使用引用来做什么/不要做什么。
      那么我会选择C ++领域中的一位重量级人物:
      Stroustrup/Sutter/Alexandrescu/Meyers

总结:

  1. 不要使用裸指针(需要所有权时)
  2. 请使用智能指针。
  3. 不要将对象复制到您的对象中(它会被切片)。
  4. 您可以使用引用(但要了解限制)。

使用引用进行依赖项注入的示例:

class Lexer
{
    public: Lexer(std::istream& input,std::ostream& errors);
    ... STUFF
    private:
       std::istream&  m_input;
       std::ostream&  m_errors;
};
class Parser
{
    public: Parser(Lexer& lexer);
    ..... STUFF
    private:
        Lexer&        m_lexer;
};

int main()
{
     CLexer  lexer(std::cin,std::cout);  // CLexer derived from Lexer
     CParser parser(lexer);              // CParser derived from Parser

     parser.parse();
}

// In test.cpp
int main()
{
     std::stringstream  testData("XXXXXX");
     std::stringstream  output;
     XLexer  lexer(testData,output);
     XParser parser(lexer);

     parser.parse();
}

谢谢,我删除了第三个例子。仍然不清楚一个问题。在我的情况下,很明显上瘾者拥有依赖关系。而且将依赖关系设置为NULL没有任何意义。这就是我想要强制执行并尽可能明确的内容。所以我应该将依赖关系作为引用传递,但将其存储为智能(scoped或shared?)指针吗?还是将其传递并将其存储为智能(scoped或shared?)指针?在类的接口中使用smart_ptr<Dependency>会不会有问题? - phtrivier
1
我会将它作为引用传递。如果没有复制或赋值,那么存储为引用。如果存在复制的可能性,则将其存储为RAW指针。只需在注释中说明您不是所有者即可。您不能使用智能指针,因为这将尝试删除对象,由于您不拥有该对象,因此不应尝试删除它。 - Martin York
1
@Martin - 看看boost::shared_ptrboost::weak_ptr一起使用,而不是使用原始指针。对于所有(可能共享的)所有权使用shared_ptr,对于依赖关系使用weak_ptr是一个非常好的组合。但是,您必须决定对于所有拥有的指针都使用shared_ptr - D.Shawley
@ Shawley:绝对没有异议:任何涉及动态内存管理的情况,应该通过智能指针来管理指针。但这并不是问题所在;依赖注入实际上并不涉及所有权。如果您将引用传递到对象中,可以将引用存储在原始指针中,__但是__您不能假定内存是动态分配的,因此不能使用智能指针来管理它。 PS. 还有一些智能指针:https://dev59.com/7nVD5IYBdhLWcg3wGHau - Martin York
@Martin:感谢您的澄清,我编辑了问题以总结我最终可能会做的事情。 - phtrivier
注意:还有一个boost:reference_wrapper<T>。这使您能够以可复制的方式存储引用。 - Martin York

6

摘要:如果您需要存储引用,请将指针存储为私有变量,并通过解除引用的方法访问它。您可以在对象的不变式中添加检查指针是否为空。

详细信息:

首先,在类中存储引用使得实现合理且合法的复制构造函数或赋值运算符成为不可能,因此应该避免使用它们。通常使用它是错误的。

其次,传递给函数和构造函数的指针/引用类型应指示谁有责任释放对象以及如何释放:

  • std::auto_ptr - 被调用函数有责任释放,并在完成时自动释放。如果需要复制语义,则接口必须提供一个clone方法,该方法应返回auto_ptr。

  • std::shared_ptr - 被调用函数有责任释放,并在完成时自动释放,并在所有其他对对象的引用消失时也会释放。如果需要浅拷贝语义,则编译器生成的函数将很好,如果需要深拷贝,则接口必须提供一个clone方法,该方法应返回shared_ptr。

  • 引用 - 调用者有责任。您不需要关心 - 对象可能被分配在堆栈上。在这种情况下,您应该通过引用传递,但是通过指针存储。如果需要浅拷贝语义,则编译器生成的函数将很好,如果需要深拷贝,则会有麻烦。

  • 原始指针。谁知道?可以分配在任何地方。可能为空。您可能需要负责释放它,也可能不需要。

  • 任何其他智能指针 - 它应该为您管理生命周期,但您需要查看文档以了解复制的要求。

请注意,赋予您释放对象的责任的方法不会破坏DI - 释放对象只是您与接口之间的合同的一部分(因为您不需要了解具体类型以释放它)。


你能详细说明一下 "如果你需要深拷贝,那就有麻烦了" 吗?如果你正在存储指向依赖项的指针,那么浅拷贝将会存储相同依赖项的地址;在任何副本都依赖于同一对象的情况下,这可能是可以接受的。在哪种情况下需要依赖项的深拷贝语义(也就是说,被复制的对象指向原始依赖项的副本)? - phtrivier
当你克隆依赖项时,需要有某个东西负责释放该副本 - 通常需要是对象的副本。如果您的原始对象不拥有其依赖项的实例,而副本则拥有,则需要额外的逻辑(以及某种标志)来跟踪您是否拥有依赖项,这非常混乱。通常可以使用浅复制,但如果您需要深度复制并且您不拥有所有成员变量,则可能已经犯了设计错误。 - JoeG
还有boost/std::tr1::reference_wrapper<T>,它模拟了一个可重新赋值的引用。它允许赋值,但在其他方面像引用一样行事,而不是指针(没有指针算术等)。 - JoeG
2
为什么原始指针的释放归属不明?如果你没有创建它,那就不应该释放它。这有什么问题吗? - user441521
原始指针不应该被归为“谁知道”的类别。当您从调用者那里收到原始指针时,这意味着调用者有责任释放它,并且调用者将确保您的生命周期与指针的生命周期一致。根据Herb Shutter的GoTW#105,传递参数时应优先使用原始指针而不是智能指针。 - Shital Shah
在这种情况下,你应该通过引用传递,但是通过指针存储。你的意思是原始指针吗? - zwcloud

1

[更新1]
如果您可以始终保证依赖项的生命周期超过使用者,那么您当然可以使用裸指针/引用。在这两者之间,我会做出一个非常简单的决定:如果允许NULL,则使用指针;否则使用引用。

(我的原始帖子的重点是,指针和引用都无法解决生命周期问题)


我会遵循臭名昭著的 Google 风格指南,在这里使用智能指针。

指针和引用都有同样的问题:你需要确保依赖项比 addict 存在更长的时间。这将一个相当恶心的责任推到了客户端。

使用(引用计数的)智能指针,策略变成了依赖项在没有人再使用它时被销毁。对我来说听起来很完美。

更好的是:boost::shared_ptr(或类似的智能指针,允许类型中立的销毁策略),策略附加到对象的构造上 - 这通常意味着影响依赖关系的所有内容都集中在一个地方。

智能指针的典型问题 - 开销和循环引用 - 很少在这里发挥作用。依赖实例通常不是微小且数量众多的,而具有强引用返回其 addicts 的依赖关系至少是代码异味。(仍然需要记住这些事情。欢迎回到 C++)

警告:我并不是“完全认可” DI,但我完全认可智能指针 ;)

[更新2]
请注意,您始终可以使用空删除器创建指向堆栈/全局实例的shared_ptr
但是,这需要双方都支持:addict必须保证不会将对依赖项的引用转移给可能存在更长寿命的其他人,而调用者则有责任确保生命周期。我对此解决方案并不满意,但有时会使用它。


2
如果你不知道指针是如何被分配的,那么“使用共享指针”的建议就没有意义 - 它所指向的东西是否已经被动态分配?很可能,被注入的对象是自动或静态创建的,在这种情况下,使用共享指针(或任何智能指针)都将是一场灾难。 - anon
好的,我通常会有两种情况:测试代码,在这种情况下,我通常会注入一个类模拟版本实例的地址,为了编写方便,我可能会在栈上创建它。在“生产”代码中,我将注入一个动态分配对象的地址。但只有执行分配的对象才拥有该对象。 - phtrivier
智能指针带来了自己的复杂性,但各有所好。关于销售,当它代表着穷人模板时,您不必购买DI。 - rama-jka toti
1
那是错误的建议,尼尔。C++标准库中的共享指针包含一个指定它们如何被删除的函数对象。如果你正在编写一个测试,并且想要将一个堆栈分配的项传递给一个需要shared_ptr的构造函数,你需要确保堆栈分配的项的生命周期将超过正在构造的对象,并将一个空操作作为删除函数传递。有趣的是,这种灵活性使得shared_ptr成为依赖注入的一个很好的例子。 - JoeG
@Neil,@phtrivier:请查看更新。@rama-jka toti:在许多情况下,我更喜欢DI(依赖注入)所做的事情,而不是模板。我只是不认为“它是解决所有问题的全新事物”。 - peterchen
依赖注入是一个相对较新的术语,但并不是一个新的想法。C++的模板可以被频繁地用作编译时依赖注入。即使模板代码可能直接创建实例,它也不需要了解除接口之外的任何类型信息。 - JoeG

1

如果你把一个对象放在STL容器中,使用引用作为成员往往会导致无尽的麻烦。我建议你考虑使用boost::shared_ptr来管理所有权,使用boost::weak_ptr来处理依赖关系。


0

这个问题之前已经被提过了,但是我的搜索技能不够好,找不到它。简而言之,你应该极少地或者从来不使用引用作为类成员。这样做会导致各种初始化、赋值和复制问题。相反,使用指针或值。

编辑:找到一个相关问题 - 这是一个有多种答案的问题:在成员数据中,我应该优先使用指针还是引用?


感谢您指向其他问题的指针。我猜“不要将引用作为成员”的团队有一种情况。 现在我仍然不知道是否应该传递指针或引用...我必须承认我没有考虑过传递引用并存储指针的选项(正如Joe所建议的)。 - phtrivier
如果您正在传递所有权(即传递的类将负责指针的生存期),请使用指针;否则,在C ++中首选的传递机制是(通常为const)引用,除了“内置”类型。 - anon

0
但是,我也得到了其他同样权威的建议,不要将引用作为成员:http://billharlan.com/pub/papers/Managing%5FCpp%5FObjects.html 在这种情况下,我认为您只想在构造函数中设置一次对象,并且永远不会更改它,因此没有问题。但是,如果以后想要更改它,请使用init函数,具有副本构造函数,总之,需要更改引用的一切都必须使用指针。

-2
我听到自己已经要被downvote了,但我要说的是,类中不应该有任何引用成员,绝对没有任何理由。除非它们是简单的常量值。这样做的原因很多,在C++中你开启了所有的坏事情。如果你真的关心的话可以查看我的博客。

你的博客中是否有特别的部分详细阐述了C++中的“坏事”? - phtrivier

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