Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ typedef versus unelaborated inheritance

Tags:

c++

stl

I have a data structure made of nested STL containers:

typedef std::map<Solver::EnumValue, double> SmValueProb;
typedef std::map<Solver::VariableReference, Solver::EnumValue> SmGuard;
typedef std::map<SmGuard, SmValueProb> SmTransitions;
typedef std::map<Solver::EnumValue, SmTransitions> SmMachine;

This form of the data is only used briefly in my program, and there's not much behavior that makes sense to attach to these types besides simply storing their data. However, the compiler (VC++2010) complains that the resulting names are too long.

Redefining the types as subclasses of the STL containers with no further elaboration seems to work:

typedef std::map<Solver::EnumValue, double> SmValueProb;
class SmGuard : public std::map<Solver::VariableReference, Solver::EnumValue> { };
class SmTransitions : public std::map<SmGuard, SmValueProb> { };
class SmMachine : public std::map<Solver::EnumValue, SmTransitions> { };

Recognizing that the STL containers aren't intended to be used as a base class, is there actually any hazard in this scenario?

like image 449
Theran Avatar asked Nov 26 '12 07:11

Theran


3 Answers

There is one hazard: if you call delete on a pointer to a base class with no virtual destructor, you have Undefined Behavior. Otherwise, you are fine.

At least that's the theory. In practice, in the MSVC ABI or the Itanium ABI (gcc, Clang, icc, ...) delete on a base class with no virtual destructor (-Wdelete-non-virtual-dtor with gcc and clang, providing the class has virtual methods) only results in a problem if your derived class adds non-static attributes with non-trivial destructor (eg. a std::string).

In your specific case, this seems fine... but...

... you might still want to encapsulate (using Composition) and expose meaningful (business-oriented) methods. Not only will it be less hazardous, it will also be easier to understand than it->second.find('x')->begin()...

like image 179
Matthieu M. Avatar answered Nov 17 '22 13:11

Matthieu M.


Yes there is:

std::map<Solver::VariableReference, Solver::EnumValue>* x = new SmGuard;
delete x;

results in undefined behavior.

like image 30
Luchian Grigore Avatar answered Nov 17 '22 13:11

Luchian Grigore


This is one of the controversial point of C++ vs "inheritance based classical OOP".

There are two aspect that must be taken in consideration:

  • a typedef is introduce another name for a same type: std::map<Solver::EnumValue, double> and SmValueProb are -at all effect- the exact same thing and cna be used interchangably.
  • a class introcuce a new type that is (by principle) unrelated with anything else.

Class relation are defined by the way the class is "made up", and what lets implicit operations and conversion to be possible with other types.

Outside of specific programming paradigms (like OOP, that associate to the concept of "inhritance" and "is-a" relation) inheritance, implicit constructors, implicit casts, and so on, all do a same thing: let a type to be used across the interface of another type, thus defining a network of possible operations across different types. This is (generally speaking) "polymorphism".

Various programming paradigms exist about saying how such a network should be structured each attempting to optimize a specific aspect of programming, like the representation or runtime-replacable objects (classical OOP), the representation of compile-time replacable objects (CRTP), the use of genreric algorithial function for different types (Generic programming), teh use of "pure function" to express algorithm composition (functional and lambda "captures").

All of them dictates some "rules" about how language "features" must be used, since -being C++ multiparadigm- non of its features satisfy alone the requirements of the paradigm, letting some dirtiness open.

As Luchian said, inheriting a std::map will not produce a pure OOP replaceable type, since a delete over a base-pointer will not know how to destroy the derived part, being the destructor not virtual by design.

But -in fact- this is just a particular case: also pbase->find will not call your own eventually overridden find method, being std::map::find not virtual. (But this is not undefined: it is very well defined to be most likely not what you intend).

The real question is another: is "classic OOP substitution principle" important in your design or not? In other word, are you going to use your classes AND their bases each other interchangeably, with functions just taking a std::map* or std::map& parameter, pretending those function to call std::map functions resulting in calls to your methods?

  • If yes, inheritance is NOT THE WAY TO GO. There are no virtual methods in std::map, hence runtime polymorphism will not work.
  • If no, that is: you're just writing your own class reusing both std::map behavior and interface, with no intention of interchange their usage (in particular, you are not allocating your own classes with new and deletinf them with delete applyed to an std::map pointer), providing just a set of functions taking yourclass& or yourclass* as parameters, that that's perfectly fine. It may even be better than a typedef, since your function cannot be used with a std::map anymore, thus separating the functionalities.

The alternative can be "encapsulation": that is: make the map and explicit member of your class letting the map accessible as a public member, or making it a private member with an accessor function, or rewriting yourself the map interface in your class. You gat finally an unrelated type with tha same interface an its own behavior. At the cost to rewrite the entire interface of something that may have hundredths of methods.

NOTE:

To anyone thinking about the danger of the missing of vitual dtor, note tat encapluating with public visibility won't solve the problem:

class myclass: public std::map<something...>
{};

std::map<something...>* p = new myclass;
delete p;

is UB excatly like

class myclass
{
public:
   std::map<something...> mp;
};

std::map<something...>* p = &((new myclass)->mp);
delete p;

The second sample has the same mistake as the first, it is just less common: they both pretend to use a pointer to a partial object to operate on the entire one, with nothing in the partial object letting you able to know what the "containing one" is.

like image 38
Emilio Garavaglia Avatar answered Nov 17 '22 11:11

Emilio Garavaglia