12

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 files are considered bad...

Solution 2:
+ loose coupling with CDoor in header (only forward declaration needed)
- memory needs to be handled by programmer

Which design do you usually prefer for what reason?

nabulke
  • 11,025
  • 13
  • 65
  • 114
  • 1
    Solution 1 is not necesarily allocated on the stack. If you create a Garage via new - the door is as well allocated on the heap. – David Feurle Nov 23 '10 at 12:00
  • @David, you are right. I just couldn't find any better words to phrase the different kinds to allocate the CDoor member. Open for any suggestions... – nabulke Nov 23 '10 at 12:04
  • @David: The point is that both objects are allocated in a single memory block. Even if it's on the heap - it's still better than 2 heap allocations. – valdo Nov 23 '10 at 12:14
  • @nabulke: I'd personally prefer 1st solution. Only in some very exceptional cases I'd use 2nd solution. – valdo Nov 23 '10 at 12:16
  • @nabulke: One more advantage of the 1st method: Accessing the member variable is fast because its offset from the pointer to the parent object is known at compile time. Plus since they're close in memory - better chance of cache utilization. OTOH the 2nd solution imposes unneeded memory indirections, and probably more memory fragmentation and cache misses – valdo Nov 23 '10 at 12:18
  • A pro for solution 1 that you forgot is cache locality, assuming CDoor isn't wrapping a pointer(even then, its still a pro) – Necrolis Nov 23 '10 at 12:19
  • You can achieve loose coupling with solution 1 as well. Look up the pimpl idiom. Typically I would use solution 2 only when CDoor is somehow requiring it. For example, you have CAutomaticDoor, CManualDoor, etc. – Daniel Lidström Nov 23 '10 at 12:23
  • @Daniel: solution 2 IS the Pimpl idiom... but I do support the polymorphic argument, polymorphism cannot be achieve with 1 directly. – Matthieu M. Nov 23 '10 at 12:35
  • @valdo, @Necrolis: let's not concern ourselves with the performance of an implementation detail, we can switch from one implementation to the other at a moment notice, so let's write correct code first, we'll measure later. – Matthieu M. Nov 23 '10 at 12:49
  • @Matthieu M: I'd say the pimpl idiom is a bit more formalized than the above, but I get your point. – Daniel Lidström Nov 23 '10 at 13:17

7 Answers7

8

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.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
2

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.

Puppy
  • 144,682
  • 38
  • 256
  • 465
1

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.

Raveline
  • 2,660
  • 1
  • 24
  • 28
  • There is a big difference, and it is a question of coupling. In the implementation of a function you do not increase it in any way by using a local-scope variable rather than auto_ptr< Foo > foo( new Foo ); etc. There would be a difference if Door was anywhere in the public interface of the class thus those who are using Garages are always going to be using Doors anyway. – CashCow Nov 23 '10 at 12:13
1

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?

Steve Townsend
  • 53,498
  • 9
  • 91
  • 140
  • yes, you are right: shared_ptr is just adding unnecessary complexity to my example. I changed it to a scoped_ptr. – nabulke Nov 23 '10 at 12:08
  • because with scoped_ptr you have an issue of not being able to use a forwardly-declared class. Sometimes you can get away with it by moving your destructor implementation out of the header. I actually prefer to often use a raw pointer here (in particular for pImpls although this is a component not a pImpl). It still obeys RAII as you delete them in your destructor. If you have more then one of them, temporarily use auto_ptr in your constructor in case an exception is thrown. At the end attach them to your members with release() (which never throws). – CashCow Nov 23 '10 at 12:08
0

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).

CashCow
  • 30,981
  • 5
  • 61
  • 92
  • I understand the issues I have when copying a class with a smart pointer member. I ommitted the copy/assignment functions/operators to keep the example simple. – nabulke Nov 23 '10 at 12:20
0

Unless two garages shares the same door, solution #1 as a shared_ptr gives the impression the door is shared.

Viktor Sehr
  • 12,825
  • 5
  • 58
  • 90
  • They would be using the same door if you copy the garage. – CashCow Nov 23 '10 at 12:15
  • Ok, perhaps this usage of a shared_ptr is giving the wrong impression. Scoped_ptr or handling the memory myself by calling new/delete could be nicer. But should I call new at all or just go with solution 1? – nabulke Nov 23 '10 at 12:15
0

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:

Community
  • 1
  • 1
Marcin
  • 897
  • 1
  • 7
  • 19