Boost logo

Boost Users :

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


Boost-users list run by williamkempf at hotmail.com, kalb at libertysoft.com, bjorn.karlsson at readsoft.com, gregod at cs.rpi.edu, wekempf at cox.net