7

I have been thinking about inheriting from STL container in C++11. I know that this should not be done without some considerations since there is no virtual destructors.

Using typedefs is, from what I have understood the preferred way to give names to STL containers.

However typedefs are not without problems by themselves. First of all they can not easily be forward declared and two typedefs might accidently be the same type.

Ponder the following:

typedef std::vector<int> vec_a_t;
typedef std::vector<float> vec_b_t;

void func(const vec_a_t& v);
void func(const vec_b_t& v);

The two functions should behave differently depending on the logical type vec_a_t or vec_b_t

This situation will work fine until someone changes vec_a_t to

typedef std::vector<float> vec_a_t;

Now a call to func() is suddenly ambiguous. A realistic example for func() is

std::ostream& operator<<(std::ostream& ost, const vec_a_t& v);

Now if we instead inherit like

class Vector : public std::vector<int>
{};

std::ostream& operator<<(std::ostream& ost, const Vector& v);

It will be possible do also declare

class Vector2 : public std::vector<int> {};

std::ostream& operator<<(std::ostream& ost, const Vector2& v);

Which will clearly be disambigious.

However since std::vector does not have virtual destructors deriving from them like this is wrong and can cause problems.

Instead we try

class Vector : private std::vector<int>
{
public:
   using::size;
   //Add more using declarations as needed.
};

Before C++11 there was issues with this also, since we would have to redeclare the constructors and it would have been possible to subclass our Vector class.

However in C++11 it is possible to do the following

class Vector final : private std::vector<int>
{
public:
   using std::vector<value_type>::vector;
   using std::vector<value_type>::size;
   //More using directives as needed.
};

From what I can see this solves a lot of the problems of why one should not derive from STL containers. It also has the following benefits:

  1. It is a distinct type, that can not cause ambiguous calls if its underlying type is changed.
  2. It can be forward declared.
  3. There is no need to re-implement forwarding methods to an internal member type.
  4. It will behave as a true STL container (if the relevant/all methods are exposed with using).
  5. Its methods can be overridden, e.g. to track calls to push_back

My questions, based on the previous discussion, are:

  • Do you see anything wrong with deriving STL containers like this in C++11?

  • Am I missing something or can this style of coding cause any problems down the line?

  • Would it cause any problems if this new type had a state of its own (e.g. track the number of calls to push_back)?

EDIT:

I know the standard answer is "Use a private field". I was wondering what the realistic downsides of the proposed solution in C++11 is? The downside of a private field is to have to re-implement a whole range of methods that just forwards to the underlying type.

This approach would not be an option either.

class Vector
{
private:
   std::vector<int> m_type
public:
   std::vector<int>& get_type(){return m_type;}
};

EDIT:

Do not use the typedef coll_t in the final solution to avoid answers that my new typedef causes problems, it was just there to ease the typing.

AxelOmega
  • 974
  • 10
  • 18
  • 4
    (My opinion) Don't do this. Make the container a private field. – Timothy Shields Jul 22 '14 at 22:23
  • 1
    Thanks, I know that this is the standard answer to this. However my question would then be why that is better then my proposed solution for C++11? – AxelOmega Jul 22 '14 at 22:26
  • Your typedef problems sound like you want Boost.StrongTypedef. – chris Jul 22 '14 at 22:28
  • Derivation is not the problem with deriving from containers; slicing is. Private derivation is one way to address that. – Nevin Jul 22 '14 at 22:33
  • 12
    The *real* answer is that there is nothing wrong with inheriting from classes without virtual destructors. Just don't try to destroy them using a base class destructor. – user541686 Jul 22 '14 at 22:44
  • 1
    If you're using C++11, you may want to consider the new `using` syntax instead of `typedef` to define type aliases. Even though they are equivalent, I find the syntax `using type=definition;` much more obvious than the old `typedef definition type;`. – Ferruccio Jul 22 '14 at 22:50
  • 1
    @Mehrdad yes it works fine, but I've found it's a code smell. You start having to worry about how the behavior of the derived class differs from the standard. I've had to deal with such code written by someone else and now I see the downsides. – Mark Ransom Jul 22 '14 at 22:51
  • @Mark Well you have a good point, but using a private field and forwarding methods will not solve this. I would say it is poor design if the interface is not honored. Like overloading operators to not do what they obviously do. – AxelOmega Jul 22 '14 at 22:54
  • @MarkRansom: Why you would even call the destructor of a container manually is beyond me. It should be treated like a value type, not allocated on the heap. So the automatic destructor should take care of its destruction, you shouldn't be deleting it regardless. – user541686 Jul 22 '14 at 22:55
  • @Mehrdad I didn't mention destructors at all, you did. I'm merely pointing out that there are other considerations besides destruction to consider. P.S. In case I wasn't clear, I wasn't talking about the behavior of the destructor, I was talking about other class behavior. – Mark Ransom Jul 22 '14 at 22:58
  • @MarkRansom: Ohh wait, but in your case, was the code actually using a base-class reference to the object at all? I can see why it would be a code smell if you try to reference it using the base class, but if you simply treat it as its own container type then the fact that it inherits from another container should be irrelevant no? – user541686 Jul 22 '14 at 23:13
  • 2
    To some people this is heresy, but I don't see a problem with it at all. And especially if you're not adding any data members to the class then it should be safe in all situations. – Galik Jul 22 '14 at 23:43
  • It isn't a good idea delete a non-virtual destructor class through a pointer. However, if you use `std::shared_ptr`, you can do that for only 1 level of inheritance. Here is the thread: http://stackoverflow.com/a/3899726/1462718 and http://stackoverflow.com/questions/3899790/shared-ptr-magic So it can actually be safe to inherit from any container; but only once. Meaning that this `shared_ptr` magic will not work for: `STLContainer->ChildContainer->ChildChildContainer`.. Only for: `STLContainer->ChildContainer`! Only one level! – Brandon Jul 22 '14 at 23:45
  • @Mehrdad they did things like overloading `operator[]` so that it would automatically grow the vector if you tried to access out of bounds. – Mark Ransom Jul 23 '14 at 01:46
  • @MarkRansom: Ohh lol, they entirely changed the semantics of the vector then. I think I've been guilty of that exact thing too, and regretted it the same way afterwards. :-) Yeah it's best not to try to change existing semantics... – user541686 Jul 23 '14 at 01:55
  • Some related reading: http://www.gotw.ca/publications/mill06.htm – juanchopanza Jul 23 '14 at 05:45
  • Overload resolution and in general, the possibility of implicit conversions, do not take into account *accessibility*. Therefore, if someone has written a `void foo(std::vector&)`, it is a viable overload for an argument of type `Vector` if you use private inheritance. (Of course, selecting this overload will fail outside of members/friends of `Vector`, which may not convert to the private base class.) – dyp Jul 23 '14 at 13:32

3 Answers3

5
struct BobsContainer {
  typedef std::vector<int> data_type;
  data_type data;
};

We now have a typed std::vector<int>. Yes, access to it requires typing .data., but in exchange we get rid of a LOT of boilerplate and nasty behavior.

If we want to construct the underlying std::vector, for implicit constructors we simply:

{ {blah, blah, blah} }

this does prefer to invoke the list initialization over standard constructors, so:

{ std::vector<int>( 3 ) }

can be used if we want to avoid them.

Hiding that you are a std::vector is relatively pointless. If your implementation is "I'm a secret std::vector and I redirect all of my methods to it", skip the secret?

It is true you can enforce some invariants by hiding some std::vector<int> but not others: but if you are going that far, go with the private solution. Writing forwarding methods, especially in C++1y, gets ridiculously easy:

template<typename... Args> decltype(auto) insert( Args&&... args ) { return data.insert( std::forward<Args>(args)... ); }

which is a bit more boilerplate than using std::vector<int>::insert;, but only a bit. And in exchange you are no longer doing strange things with 'is-a' and inheritance.

For methods with both const and non-const overloads:

template<typename... Args> decltype(auto) insert( Args&&... args ) const { return data.insert( std::forward<Args>(args)... ); }

and if you want to get really silly, include && and & overloads (standard containers don't use them yet).

So if you are forwarding almost everything, just contain a vector. If you are hiding almost everything, just contain a private vector and forward. Only in the strange unstable case where you are hiding about half of the class and exposing the other half does the using container; and private inheritance get reasonable.

Composition via inheritance is important when you want to exploit the empty base class optimization in generic code. It usually isn't a good idea otherwise. None of the standard containers where designed to be inherited from.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    I have always thought if private inheritance as has-a. While public inheritance is is-a. And with making it a class I can impose some restrictions on it. Like if I only want it to have insert have access to it with begin(), end(). To be used for e.g. for_each. – AxelOmega Jul 23 '14 at 00:44
  • 1
    `insert(Args&&...) const;`?? It strange to add new element to `const` vector. – Yankes Jul 23 '14 at 09:38
  • @yankes I don't discriminate on the basis of `const` ;) But ya, the point was to show how easy manual forwarding is. (barring rvalue references to `this`) in general. – Yakk - Adam Nevraumont Jul 23 '14 at 13:55
  • One should probably return `decltype(auto)` in the general case to preserve references from `back` etc. – Stuart Olsen Jul 23 '14 at 14:15
3

There are several ways to solve the problem with the overloads of func. If you even agree that it is a problem; I don't: "if someone changes typedef foo A; to typedef bar A; my program might stop compiling" is not something that can be avoided. The person changing the typedef has the responsibility to check what they are doing.

Anyway, a solution is to use:

template<typename T> void func(const std::vector<T> &v)

and the function can then internally call an overloaded function when it needs different functionality for int than for float.


typedef std::vector<int> coll_t;

Your proposed solution has exactly the same problem you were trying to avoid! Presumably you also need versions coll_a_t and coll_b_t, leading to VectorA and VectorB so that you can then take advantage of the overloaded func.

But if someone changes this typedef to std::vector<float> ?

M.M
  • 138,810
  • 21
  • 208
  • 365
  • I heard somewhere that you should overload (rather than template) free functions, but I'm not sure what the reasoning was (I'm a C++ noob). – dreamlax Jul 22 '14 at 22:36
  • 1
    I think you are referring to the advice to avoid *specialization* of function templates. I glossed over this on purpose to keep my post clear, but in this solution you can avoid a partial specialization by using tag dispatch instead (i.e. the implementation of `func` depends in part, or wholly, on a non-template overloaded function). – M.M Jul 22 '14 at 22:40
  • Ah yep that was it... specialisation of function templates. – dreamlax Jul 31 '14 at 08:13
-1

Typedefs are completely fine. If you accidentaly have two different typedefs for the same type, your code structure is wrong.

However, if there is some reason for which you cannot guarantee typedefs uniqueness, you can try the following: using std::type_traits, you can check whether two typedefs mean exactly the same. If yes, you can use std::enable_if to shadow second declaration.

constexpr bool isSame = std::is_same<vec_a_t, vec_b_t>::value;
void func(const vec_a_t& v);
template <typename = std::enable_if<!isSame>::type> func(const vec_b_t& v);
Garrappachc
  • 719
  • 5
  • 19
  • The point was that vec_a_t and vec_b_t should always be distinct functions. func(vec_a_t&) should always do different things then func(vec_b_t&) – AxelOmega Jul 22 '14 at 22:48