Wednesday, 9 March 2011

Things I Hate Doing in C++

Why do I do it to myself? Manage memory, manage low-level services, write out loops time and time again, hand-roll all of my thread locking, monitor pattern stuff and observers?

Because I've forgotten to behave like the "real engineer" I claim to be, that's why. If I was behaving the way I was taught (as a mechanical engineer), I wouldn't revisit the same problem over and over, I'd capture a solution once and for all and apply the pattern repeatedly, re-factoring it slightly to accommodate edge cases as I discover them. Now I know that we do this sort of thing in C++ already to some extent: that's what the STL's for. And that's what boost picks up and runs on with for miles more. But it's still not enough. I was also reminded recently that one of my esteemed colleagues had written a multi-methods pre-compiler for C++ (http://www.op59.net/cmm/readme.html), solving an age old problem, and I though, heck, why don't we just do that for all common language problems?

So, here's the list of things that I (or we) could start writing pre-compile scripts for:
  • Pre-conditions, post-conditions and invariants. Code contracts are a good example of this in C#.
  • Exception safety, so it's not hideously cryptic.
  • Thread access models, perhaps like declaring things shared in D. A simple type modifier would be great here wouldn't it? Default everything to "local" and require people to write "shared" where they need it and make the pre-compiler barf at non-threadsafe access or mutation of shared objects.
  • Other common thread patterns like Monitor could perhaps be boiler-plated away in a pre-compiler.
  • And indeed other very common design patterns like Observer and Adapter could be too.
  • Inheritance concepts like "abstract" and "final" from Java and C# are extremely useful and can prevent hideous maintenance bugs from creeping in as people extend classes that they weren't supposed to.
Finally, perhaps some static language specification problems could be avoided using a pre-compile check, such as:
Abstract base classes must have a public virtual destructor or a non-public non-virtual one
Container access should be "protected" unless officially declared to be "unsafe"
That's by no means a complete list, and perhaps some of the concepts are completely impossible, but it's worth a try right? My vision here is that I could write a snippets like the following with ease, capturing almost all of the important design decisions in interfaces, allowing readers and maintainers of my code to proceed with confidence and without fear of introducing bugs:

How about observation?
class Sink;
class Parser
{
  #observed( Sink, void OutputString( const std::string & ) );
public:
  void Parse( std::string & i_rString )
  {
    // ...
    #notify_observers
  }
};
class Sink
{
  #observes( Parser );
public:
  Sink( Parser & i_rParser )
  : #observe( i_rParser )
  {
  }
  void OutputString( const std::string & i_rString )
  {
     std::cout << i_rString << std::endl;
  }
};

Now how about pre- and post-conditions?
std::string Translate( const std::string & i_rString )
  #precond( ! i_rString.empty() )
  #postcond( ! #return.empty() );

Inheritance?
class Base #abstract { };
class Derived : public Base #final { };

My wish to handle some static language assertions here could be dealt with: that #abstract base class should have a public virtual destructor, or a non-virtual one must be declared protected. The "unsafe" vector dereference could be handled something like:
std::vector< int > Vector( 10, 0 );
#unsafe( Vector[0] = -1 );

  // All OK: you've declared your risky intentions clearly
#protected( Vector, Vector[20] = 10 );

  // Will be OK: the write won't happen
Vector.at( 100 ) = -100;

  // Pre-compile error: declare unsafe or protected!
Thread access patterns are quite complex, but nonetheless, wouldn't this be nicer?
class Harness
{
public:
  void HandleString( const std::string & i_rString )
  {
    std::string String = Translate( i_rString );
    #threadsafe m_Parser.Parse( String );
  }
private:
  #shared Parser m_Parser;
};

And finally, exception safety (which I've tackled previously in a couple of easy-to-use scoped macros that look almost the same: I must publish those):
struct POD
{
  std::string m_Name;
  std::string m_FrenchName;
  void SetName( const std::string & i_rName )
  {
    #exceptionsafe( m_Name, m_FrenchName );
    {
      m_Name = i_rName;
      m_FrenchName = Translate( i_rName ); // Might throw???
    }
  }
};

All looking good to my eyes. Perhaps not to yours, but the important thing here isn't beautiful language specification, it's capturing the things I know I do, and I know I do badly (by making mistakes typing it all in over and over). Generic code is all well and good, but it still has to be be written within the confines of the language, which can often result in incredibly cryptic expressions. And they are very high level concepts I'm trying to capture here, not low ones, and most importantly, I'm trying to write less code, because I'm the source of the errors, not my compiler.

No comments:

Post a Comment

Post a Comment