2017-05-28 3:02 GMT+02:00 Emil Dotchevski via Boost
On Sat, May 27, 2017 at 4:09 PM, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
2017-05-28 0:55 GMT+02:00 Emil Dotchevski via Boost < boost@lists.boost.org
:
On Sat, May 27, 2017 at 3:15 PM, Andrzej Krzemienski via Boost <> Note that there is no provision to report a failure (to establish the invariants) except by throwing. This is not an omission but a deliberate design choice.
Interesting. I do not know what you mean here. Maybe, "there is no other way to report such failure except to throw an exception"? If so, I agree, but how does this relate to the discussed topic?
What I am demonstrating is that the C++ semantics for initialization and destruction of objects have a built-in assumption about what constitutes a valid object. Can you define this as "the only safe thing to do with x is call is_valid()" on it? Sure, but then you're operating as a C programmer. Consider what a C programmer has to do:
struct foo { /*state*/ };
void init_foo( foo * x ) { /*initialization*/ assert(is_valid(x)); }
void destroy_foo( foo * x ) { assert(is_valid(x)); //destroy *x }
void use_foo( foo * x ) { assert(is_valid(x)); /*use foo*/ }
And now compare this to a C++ program:
struct foo { /*state*/ foo() { /*initialization*/ }
~foo() { /*destruction*/ }
void use() { /*use foo*/ } };
Note that in well designed C++ programs not only the asserts are not necessary, they're downright silly. Is the object valid? Duh, of course it is, or else the constructor would not have returned. But if you introduce a "not quite valid" state, not only you need the asserts, you're making it much more difficult for the user to reason about the state of the objects in his program, just like a C programmer must.
Emil, thanks for being patient with me. I understand what you are saying here. It is convincing. But ultimately I have found it to be incorrect after C++11 introduced move semantics. Let me show you a typical implementation of a RAII-like type for representing file-handles. First, in C++03, without moves ``` class File { int _handle; public: explicit File(string_view name) : _handle(system::open_file(name)) { if (_handle == 0) throw FileProblem{}; } char read() { // no precondition: _handle always valid return system::read_char(_handle); } ~File() { // no precondition: _handle always valid system::close(_handle); } }; ``` It is as you say: if we have an object, we know we have a file open, ready to be used. But now, lets's add C++11's move semantics: ``` class File { int _handle; public: explicit File(string_view name) : _handle(system::open_file(name)) { if (_handle == 0) throw FileProblem{}; } File(File && rhs) : _handle(rhs._handle) { rhs._handle = 0; // now rhs obtains an invalid state (or, not-a-file state) } char read() { // precondition: _handle != 0 return system::read_char(_handle); } ~File() { if (_handle) // defensive if system::close(_handle); } }; ``` Now, because I have a moved-from state, it weakens all my invariants. As you say, every function now has a precondition: either I put defensive if's everywhere, or expect the users to be putting them. And this is a normal moveable RAII class (or maybe a movable type is no longer "RAII" because of this). And we have lived with it for years now. I often have functions returning std::unique_ptr's, and I am not defensive-checking everywher if the function did not return a null. I just trust that if someone is returning a unique_ptr it is because they wanted to return a heap allocated object: not null. My point: moved-from state is quite similar to valueless_by_exception, it exposes the same problems (weak invariants on RAII-like types), and no-one complains about it.
If the user is handed an object, is it safe to use it? He could assume that it is, but your design choice has rendered that an unsafe assumption, because you're effectively reserving the right for the object to be unusable. Alternatively, he could assume that he can't use it unless he checks first. But now he's forced to write if( obj.is_valid() ) obj.use() rather than obj.use().
Same with all movable types. And we use them, and I don't think we recommend putting defensive if-s everywhere in the code.
What he is supposed to do in theory is analyze the program carefully and determine for sure if in this particular case the object can be unusable. In practice, there are so many corner cases that it is easy to get this wrong, if not when the code is initially written, then under maintenance. Such bugs are very difficult to detect, and infinitely more difficult to detect in error handling code (where this "not quite valid" state would occur, per your design.)
Again, I understand your reasoning, but I think it equally well applies to moved-from state. Are you also describing shortcommings of moves? Regards, &rzej;