Le 29/05/2017 à 00:15, Andrzej Krzemienski via Boost a écrit :
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:
: On Sat, May 27, 2017 at 3:15 PM, Andrzej Krzemienski via Boost <> Note
2017-05-28 0:55 GMT+02:00 Emil Dotchevski via Boost < boost@lists.boost.org 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. Not exactly. moved-from state has finished its operation move with success. While valueless_by_exception has changed the state on a failure.
Vicente