[I think I'm mostly repeating what other people have said here, but decided to post anyway] 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?
Essential, but there's no rule without an exception.
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.
Actually, I think the above is an example of a class that's easy to test in isolation. In fact, close to perfect ;-) You're requiring a reference to a polymorphic object in the ctor, which is great from a testing point of view. It's possible to provide a ostringstream if you want to test the formatting, or some variety of "nullstream" if you want to test other stuff. Dependency injection via ctors are my preferred way of making (C++) objects testable. Also, in this very case the ostream isn't provided just for testability - it's an essential part of the interface. For your user requirement (~"needs an ostream for which to log events") there's really no good default either, as e.g. std::cout requires a console application. Perhaps a different example would be better to avoid this distraction?
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....)
For the general case, that's true. For this specific case, I don't agree. I'm a bit curious though, about what constitutes a test from your point of view. From time to time in your posting, I'm not getting the grip of whether you are talking about unit testing from the authors point of view, or exploratory testing.
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.
If the constructor arguments (or rather, their contributions to the functional state of the object) are essential for the functionality of the object, they shouldn't have unusable defaults - i.e. cause violations of later method call preconditions. As a developer I occasionally find myself adding extra arguments to ctors, or ctors overloads, just to make the dang thing testable without having to access an external resource, such as the file system, or the underlying OS API. For those cases, where the extra argument or overload are there just for the sake of testability, there always exists a reasonable default (or perhaps even only one real implementation). I just try to make sure that those extra arguments won't have to be provided by the casual user. I can agree that this last thing is a kind of interface pollution, but IMHO it is essential to be able to test as much as possible in isolation.
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.
I think your gut instinct is correct. Also, taking design guidelines for .NET (which are perhaps absolutely appropriate there) and attempting to apply them to C++ programming might not be the right way to go.
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
Yuk. If there was really a need for this lazy init, I think a null outputter would be better in this case. But it would depend on the application in question. [snip lots of .NET discussion]
So I'm confused. Constructors that "really" initialize objects detect some kind of errors during compilation, but they make testing harder,
Again, if the ctor arguments are essential for the operation of the object and have no reasonable defaults, what's the alternative, really? If the arguments are non-essential, don't require them, but perhaps provide additional overloads to allow customized construction. An example for latter would be the inclusion of a filter for the EventLog class, e.g. based on message contents or priority (as I believe someone else said also).
are arguably contrary to exploratory programming,
I might be saying the same thing over and over again, but how can you explore something unusable? As a side not though, I often prefer using unit testing as an exploratory tool.
and seem to contradict the advice of the designers of the .NET API.
Different platform and philosophy, unless you're talking about C++/CLI.
Constructors that "sort of" initialize objects are more test-friendly (also more loosely coupled, BTW)
I don't understand how loose coupling and "sort-of-initialized" objects connect?
and facilitate an exploratory programming style, but defer some kinds of error detection to runtime (as well as incur the runtime time/space costs of such detection and ensuing actions).
So, library users, what do you prefer, and why?
As above. Regards, Johan Nilsson