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