wt., 2 kwi 2019 o 22:59 Peter Dimov via Boost
Andrzej Krzemienski wrote:
Can you show a destructor like that, does a visitatoin on a variant, and that you wouldn't be ashamed to put in your program?
The destructor doesn't have to do visitation. It merely needs to call a function f1, which calls a function f2, which calls a function f3, which calls a function f4, which does visitation.
What we've been telling you from the start is that this forces you to
partition your functions into two classes, one allowed to do visitation, the other not, then keep track of which is which, never calling the wrong class in a destructor, or from a catch clause.
The fact that I have to partition all functions into two categories isn't new or surprising. When defining an operation that needs to provide a strong exception-safety guarantee I need to some use operations that offer no-fail guarantee. So I need to be aware which operations offer no-fail guarantee and use only those. I do the partitioning, and I need to be cautious. When I define the destructor, again I do not have the liberty to use any function I can use in other parts of the code. In destructor I can only use functions that do not signal failure through exceptions if they fail. And not signalling failures is ugly on its own, so I am inclined to also just use functions that cannot fail. I have to do the partitioning again. When I try to go back and recall how the destructors I write look like, they fall into three categories: 1. Some types just aggregate other types. The implicitly defined destructor only calls the destructors of subobjects. 2. Sometimes I need to provide a RAII interface for a system resource or a C component. In that case my implementation uses only ints and pointers and system calls. When defining the destructor, I have confidence that there are no unexpected types hidden by templates, or functions with unexpected types with unpredictable guarantees. 3. When I define a generic component, such as optional<T>, in my destructors the only operation on the unknown type T is to call its destructor. Other operations in the destructor are performed on concrete types with no-fail guarantee such as `bool`. The situation is no different for std containters: for T, they only call destructors, other operations are on concrete types: pointers, memory deallocation. No way a variant or other unexpected type could get there, even indirectly. I know people are using destructors for other things, like committing the DB transaction, but this is what I call bad exception handling. Such implementations are usually incorrect: either you do not report failures in such case, or you are reporting failures when there is none. Your example of a destructor, to me, also falls into this category: template<class T> class monitor
{ char const* file_; int line_; T const& v_; T old_;
public: explicit monitor( char const* file, int line, T const& v ): file_( file ), line_( line ), v_( v ), old_( v ) {}
~monitor() { if( v_ != old_ ) { std::clog << file_ << ":" << line_ << ": monitored value changed from " << old_ << " to " << v_ << std::endl; } } };
It calls operations on an unknown T that could fail and throw: comparison, streaming. Streams themselves could throw. Such implementation has other problems before we even consider a variant. And, of course, this has nothing to do with visitation, specifically. The
exception safety guarantee is a global thing. Once you lose basic, you lose it everywhere, not just for visit.
Why are you mentioning "basic" (guarantee) here? In the vision that I propose variant offers basic guarantee. It is just that the invariant is weaker. This is analogous to a (potentially smart) pointer: normally you can call `*p`, but sometimes (on nullptr) it is UB, so you may need to check what state it is in. Regards, &rzej;