“Avoid returning handles to object internals”, so what's the alternative?

天大地大妈咪最大 提交于 2019-12-18 03:17:33

问题


Effective C++ by Scott Meyers tells in Chapter 5, Item 28 to avoid returning "handles" (pointers, references or iterators) to object internals and it definitely makes a good point.

I.e. don't do this:

class Family
{
public:
    Mother& GetMother() const;
}

because it destroys encapsulation and allows to alter private object members.

Don't even do this:

class Family
{
public:
    const Mother& GetMother() const;
}

because it can lead to "dangling handles", meaning that you keep a reference to a member of an object that is already destroyed.

Now, my question is, are there any good alternatives? Imagine Mother is heavy! If I now return a copy of Mother instead of a reference, GetMother is becoming a rather costly operation.

How do you handle such cases?


回答1:


First, let me re-iterate: the biggest issue is not one of lifetime, but one of encapsulation.

Encapsulation does not only mean that nobody can modify an internal without you being aware of it, encapsulation means that nobody knows how things are implemented within your class, so that you can change the class internals at will as long as you keep the interface identical.

Now, whether the reference you return is const or not does not matter: you accidentally expose the fact that you have a Mother object inside of your Family class, and now you just cannot get rid of it (even if you have a better representation) because all your clients might depend on it and would have to change their code...

The simplest solution is to return by value:

class Family {
public:

    Mother mother() { return _mother; }
    void mother(Mother m) { _mother = m; }

private:
    Mother _mother;
};

Because in the next iteration I can remove _mother without breaking the interface:

class Family {
public:

     Mother mother() { return Mother(_motherName, _motherBirthDate); }

     void mother(Mother m) {
         _motherName = m.name();
         _motherBirthDate = m.birthDate();
     }

private:
     Name _motherName;
     BirthDate _motherBirthDate;
};

See how I managed to completely remodel the internals without changing the interface one iota ? Easy Peasy.

Note: obviously this transformation is for effect only...

Obviously, this encapsulation comes at the cost of some performance, there is a tension here, it's your judgement call whether encapsulation or performance should be preferred each time you write a getter.




回答2:


Possible solutions depend on actual design of your classes and what do you consider "object internals".

  1. Mother is just implementation detail of Family and could be completely hidden from Family user
  2. Family is considered as composition of other public objects

In first case you shall completely encapsulate subobject and provide access to it only via Family function members (possibly duplicating Mother public interface):

class Family
{
  std::string GetMotherName() const { return mommy.GetName(); }
  unsigned GetMotherAge() const { return mommy.GetAge(); }
  ...
private:
   Mother mommy;
   ...
};

Well, it can be boring if Mother interface is quite large, but possibly this is design problem (good interfaces shall have 3-5-7 members) and this will make you revisit and redesign it in some better way.

In second case you still need to return entire object. There are two problems:

  1. Encapsulation breakdown (end-user code will depend on Mother definition)
  2. Ownership problem (dangling pointers/references)

To adress problem 1 use interface instead of specific class, to adress problem 2 use shared or weak ownership:

class IMother
{
   virtual std::string GetName() const = 0;
   ...
};

class Mother: public IMother
{
   // Implementation of IMother and other stuff
   ...
};

class Family
{
   std::shared_ptr<IMother> GetMother() const { return mommy; }
   std::weak_ptr<IMother> GetMotherWeakPtr() const { return mommy; }

   ...
private:
   std::shared_ptr<Mother> mommy;
   ...
};



回答3:


If a read-only view is what you're after, and for some reason you need to avoid dangling handles, then you can consider returning a shared_ptr<const Mother>.

That way, the Mother object can out-live the Family object. Which must also store it by shared_ptr, of course.

Part of the consideration is whether you're going to create reference loops by using too many shared_ptrs. If you are, then you can consider weak_ptr and you can also consider just accepting the possibility of dangling handles but writing the client code to avoid it. For example, nobody worries too much about the fact that std::vector::at returns a reference that becomes stale when the vector is destroyed. But then, containers are the extreme example of a class that intentionally exposes the objects it "owns".




回答4:


This goes back to a fundamental OO principle:

Tell objects what to do rather than doing it for them.

You need Mother to do something useful? Ask the Family object to do it for you. Hand it any external dependencies wrapped up in a nice interface (Class in c++) through the parameters of the method on the Family object.




回答5:


because it can lead to "dangling handles", meaning that you keep a reference to a member of an object that is already destroyed.

Your user could also de-reference null or something equally stupid, but they're not going to, and nor are they going to do this as long as the lifetime is clear and well-defined. There's nothing wrong with this.




回答6:


It's just a matter of semantics. In your case, Mother is not Family internals, not its implementation details. Mother class instance can be referenced in a Family, as well as in many other entities. Moreover, Mother instance lifetime may even not correlate with Family lifetime.

So better design would be to store in Family a shared_ptr<Mother>, and expose it in Family interface without worries.



来源:https://stackoverflow.com/questions/13176751/avoid-returning-handles-to-object-internals-so-whats-the-alternative

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!