design class aggregation - stack allocation vs dynamic memory allocation
Please have a look at the two simplified examples of designing a class aggregation below.
Solution 1
Header
// need include, forward declaration is not enough
#include "door.h"
class CGarage
{
public:
CGarage(const std::string &val);
private:
CDoor m_door;
};
Source
#include "garage.h"
CGarage::CGarage(const std::string &val)
:m_door(val)
{
}
Solution 2
Header
#include "smart_ptr.hpp"
// forward declaration
class CDoor;
class CGarage
{
public:
CGarage(const std::string &val);
private:
scoped_ptr<CDoor> m_door;
};
Source
#include "garage.h"
#include "door.h"
CGarage::CGarage(const std::string &val)
:m_door(new CDoor(val))
{
}
Questions concerning the creation of the CDoor member
What advantages/disadvantages do you see in the design of the examples (dynamic allocation of CDoor vs automatic allocation)?
This is what I came up with:
Solution 1:
+ no issues with memory handling or lifetime + no need for expensive memory allocation at runtime - need additional include in header (compilation speed slower?, closer coupling to CDoor) -> many includes in header file开发者_StackOverflow社区s are considered bad...Solution 2:
+ loose coupling with CDoor in header (only forward declaration needed) - memory needs to be handled by programmerWhich design do you usually prefer for what reason?
It is rare that we get question design (I mean, interesting ones).
Let's forget for a moment the (obviously) contrived example and concentrate on the notion.
We have 2 solutions:
- Hard containment: pull in the header and build the object directly
- Soft containment: forward declare the header and use a pointer
I'll voluntarily discard all "performances" argument for the moment. Performance doesn't matter 97% of the time (says Knuth) so unless we measure a noticeable difference, since the functionality is identical, we thus need not worry about it at the moment.
We therefore have two orthogonal concepts attempting to sway our decision:
- Dependency make us lean toward Soft containment
- Simplicity make us lean toward Hard containment
Some answers here have rightly spoken about polymorphism, but the exact implementation of Door
is a detail that is Door
's concern, not Garage
's. If Door
wishes to offer several implementations, that's fine, as long as its clients need not be concerned by this detail.
I am quite a fanboy, myself, of the KISS and YAGNI principles. So I would argue in favor of Hard containment... with one caveat.
When designing an interface that will be exposed, an interface therefore that stands at the frontier of the library, then this interface should expose a minimum of dependencies and internals. Ideally, this should be a Facade
or a Proxy
, an object whose only purpose is to hide the internals of the library, and this object should have minimal dependencies in its header and have maximal layout compatibility, which means:
- no virtual method
- a simple pointer as an attribute (Pimpl)
For all internal classes, simplicity wins hands off.
Solution 1 is superior at both run and compile-time in every conceivable case, unless you're having extreme issues with include dependencies and must act to reduce them. Solution 2 has more issues than you've mentioned - you'll need to write and maintain additional copy constructor/assignment operator, just to begin with.
To me these designs are equivalent. In each case CDoor
is owned by CGarage
.
I prefer 1. since the shared_ptr
in the second does not seem to add anything but complexity - who is CGarage
sharing it with? Your cons for 1. are not compelling to me.
Why not use scoped_ptr
in 2. unless you are providing a getter for the CDoor
object?
It's not only a question of coupling (far from it, actually : dynamical allocation becomes really interesting if you use polymorphism). The classical rule of thumb, there, is : if you can have the object in your class, do it. It's exactly the same thing as in a function : if you can have a local variable, take it, don't go allocating memory for the sake of nightmarish debugging.
For instance, if your going to need an aggregation of an unknown numbers of components, pointers (shared, smart, or dumb) are your friends. Here, for instance, if you don't know how much doors your garage is going to have, pointers (actually, not shared ones) and dynamical allocation is a good idea.
If you have an object used by another object, that is always going to be of the same class, and that is not useful after it's owner is dead, why on earth would you need to go through dynamic allocation ?
In short : the context is everything, but, for your own sake, try to have as few dynamic object as possible.
Unless two garages shares the same door, solution #1 as a shared_ptr gives the impression the door is shared.
Solution 1:
You are exposing the header of Door which is only an implementation detail of your class. If Door was part of the public interface of Garage you might assume users of Garage are going to use Door too, but where it is private, it is far better not to be exposed.
Solution 2:
Using shared_ptr it means if you copy a Garage your copy has the same door. Not just as similar one, the same one. If you paint the door green on one of your garages, both your garages will have green doors. You must understand that issue.
Where your class sits in your code plays a major part as to which is better to use. If Garage is part of your public interface and Door is not anywhere in the public interface at all then it is very much beneficial to decouple it out, possibly using shared_ptr.
If Garage is not part of your public interface anywhere but is an implementation detail I would not care so much about the coupling issue.
If Garage and Door are both in your public interface. and Door is very commonly used with Garage,.and Door.h does not bring in even more headers but is fairly light, you can get away with the aggregation as an object (include the header).
Some more points to consider:
Solution 1 (assuming that CDoor is not a typedef for a pointer type):
- Is not "polymorphism friendly" since you will copy objects by value on initialization (even if you pass by reference). Please see "class slicing" issue: What is object slicing?
- You can not implement pimpl idiom for fast coping/initialization of CGarage
In general, (1) means that CGarage is tightly compled with CDoor. Of course, you can achieve some more flexiblity if CDoor is some kind of adapter/decorator
Solution 2:
- Classes coupled less tightly
- Expensive heap allocation
- Additional costs for smart pointer
Neither design can be preferred "usually" this depends entirely on what is your class modeling what it's responsible for and how it will be used.
If a may advise you further, please research "C++ design patterns" subject to get some more insight.
Those should be good for starters:
- The gang of four classics
- Wikipedia
- Faqlite
精彩评论