$include_dir="/home/hyper-archives/boost-users/include"; include("$include_dir/msg-header.inc") ?>
From: Rush Manbert (rush_at_[hidden])
Date: 2006-09-12 13:50:27
Scott Meyers wrote:
> I have a question about library interface design in general with a 
> strong interest in its application to C++, so I hope the moderators will 
> view this at on-topic for a list devoted to the users' view of a set of 
> widely used C++ libraries.  In its most naked form, the question is 
> this:  how important is it that class constructors ensure that their 
> objects are in a "truly" initialized state?
> 
> I've always been a big fan of the "don't let users create objects they 
> can't really use" philosophy.  So, in general, I don't like the idea of 
> "empty" objects or two-phase construction.  It's too easy for people to 
> make mistakes.
<snip>
> 
> I've slept soundly with this philosophy for many years, but lately I've 
> noticed that this idea runs headlong into one of the ideas of 
> testability:  that classes should be easy to test in isolation.
<snip>
It seems to me that if I am the library developer, then I will care 
about the ease of testing individual objects in isolation. However, if I 
am a library user, I really don't care about testing individual objects. 
(The library will either come with a test suite that I can run, or I 
will trust it [at least initially] because of the source) I want to use 
the library, and I want the public API to make it as hard as possible 
for me to make silly mistakes.
I have been in the situation where testing required a great amount of 
infrastructure. It's hard to deal with, but I don't think that providing 
dumbed down object constructors is a very good idea because:
1) You can generally factor out the portion of your class that can be 
tested without the infrastructure overhead, either through inheritance 
or aggregation. Then testing that part is still *easy*.
2) If you're serious about testing, you're going to need all the 
infrastructure anyway. If the object requires an ostream to do its job, 
then you need to figure out how to give it an ostream
> 
> Another thing I've noticed is that some users prefer a more exploratory 
> style of  development:  they want to get something to compile ASAP and 
> then play around with it.  In particular, they don't want to be bothered 
> with having to look up a bunch of constructor parameters in some 
> documentation and then find ways to instantiate the parameters, they 
> just want to create the objects they want and then play around with 
> them.  My gut instinct is not to have much sympathy for this argument,
> but then I read in "Framework Design Guidelines" that it's typically 
> better to let people create "uninitialized" objects and throw exceptions 
> if the objects are then used.  In fact, I took the EventLog example from 
> page 27 of that book, where they make clear that this code will compile 
> and then throw at runtime (I've translated from C# to C++, because the 
> fact that the example is in C# is not relevant):
> 
>    EvengLog log;
>    log.WriteEntry("Hello World");    // throws: no log stream was set
> 
> This book is by the designers of the .NET library, so regardless of your 
> feelings about .NET, you have to admit that they have through about this 
> kind of stuff a lot and also have a lot of experience with library users.
Okay, so I can construct a useless object and get an exception thrown if 
I use it. Why is this useful to me? If I go ahead and write my code as 
if the object were properly constructed, then my code won't run. If I 
add a bunch of fake code to catch the exceptions, then I've wasted a lot 
of time and effort writing useless code.
In  a case like the example, where I know it might be hard to setup the 
object correctly, I'd rather have a constructor that took some bogus 
argument type that set the object up in "simulated success" mode, but 
only in my debug build. That way I could defer figuring out how to 
provide the real constructor arguments until I really needed the object 
to do what it normally does. (Sometimes you really do just want to get 
on with writing your code, knowing that you need to come back to solve 
these sorts of problems.) But if I forget to do that and never correctly 
construct the object, then my release build should throw in the constructor.
So my code would look like this:
     EventLog log (true);  // FIXME_rush - Use correct constructor!
     log.WriteEntry ("Hello World"); // Just asserts the ptr arg
and the relevant constructor would look like this:
EventLog::EventLog (bool dummyarg)
{   // This constructor sets us up in simulated success mode.
     #ifdef DEBUG	// or whatever you use
         m_simulateSuccess = true;
     #else
         throw something useful
     #endif
}
and all the other constructors initialize m_simulateSuccess to false.
Lastly, WriteEntry looks like this:
void EventLog::WriteEntry (char const * const pLine)
{
     assert (pLine);
     if (! m_simulateSuccess)
     {	// The real code is here
     }
}
Now that I've written all that and read it over, the default constructor 
could be the one that sets up simulated success mode, since it's really 
invalid for properly constructing the object. I'll leave that as an 
exercise for the reader. ;-)
Just one guy's opinion.
Best regards,
Rush