
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?
very important in my view
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. So, for example, if I were to design an interface for an event log that required a user-specified ostream on which to log events, I'd declare the constructor taking an ostream parameter (possibly with a default, but that's a separate question, so don't worry about it):
EventLog::EventLog(std::ostream& logstream); // an ostream must be // specified
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. The above constructor requires that an ostream be set up before an EventLog can be tested, and this might (1) be a pain and (2) be irrelevant for whatever test I want to perform. In such cases, offering a default constructor in addition to the above would make the class potentially easier to test. (In the general case, there might be many parameters, and they might themselves be hard to instantiate for a test due to required parameters that their constructors require....)
Testing in isolation make sense if you are the writer of the library, not as the user if it. Internal (none-public) interfaces are appropriate for such use-cases. As a library user I prefer to trust the library, if I conclude it is no reason to trust it, I prefer to find alternatives to using that particular library. Confusing interfaces are to me traps waiting to take away my trust. It does not help that intentions are good and that there may exist valid use-cases. Confusing interfaces are still confusing. Why mess up an otherwise good external interface of your component with confusing junk for the sake of testability. If you really need such an interface, make it a separate one. This "special needs" interface should not be the first that pops up in the face of library users. Hide it so only specialist looking for it will find it, and make sure they are aware what territory they are entering. Preferably, in my view, such interfaces should be internal to your component, hence supporting testing and other needs for the library writer without messing up the API.
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.
Ok, sounds good and perfectly reasonable,
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.
I can not see how adding confusing constructors or other confusing methods in the interface would help anything. If I explore new territory I certainly would like to be able to make valid assumptions about the objects I use based on intuition. If I think I test-drive a snow-mobile, and don't realize I have forgot to add a belt to it, then I have no idea of what I am exploring. A reasonable default behavior must be the better solution for exploring.
My gut instinct is not to have much sympathy for this argument, but
agreed
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.
I would much prefer build time diagnostics if possible. That said, sometimes you need data holder objects which potentially have costly default constructors. If in general the state of the object after default initialization is legal, but not very useful, then there may be a better idea to give the object a defined not_valid state into which the default construction takes it. If that is done, throwing upon access of the object may be an OK solution. Users could also by use of policies decide if exceptions are thrown or explicit checking of an is_valid() member should be used. Note that there is a clear distinction here between uninitialized as in undefined, and uninitialized as in a defined not_valid state. The latter is in my view only a good solution for classes used to hold data. Either as optimalization or more often as means to aid application logic. The former is never a good solution.
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
It does not make any sense to me why a reference to std::cout or something similar could not be used as the default stream here. I fail to see the benefit of the solution in the code example above.
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.
I am not convinced they have through so much about this, they may have - but I am not convinced. It may not be wise to assume the solution is good based on an assumption that some really smart people have thought long and hard about these aspects of the API usability. Even if that was the case and this in fact is the best solution for C#, the reasoning behind it may not apply to C++. These types of interfaces are nothing new, maybe the part that throws on uninitialized use is of newer date. But except from that, this looks like patterns both library developers and users has been used to since long before OO and C++ caught fire. I am afraid we are so used to it that we miss the chance to see and call out how bad it looks. ------ Bjørn Roald