
Howard Hinnant wrote:
On Apr 16, 2007, at 4:32 PM, Ion Gaztañaga wrote:
I'm terrible explaining my points.
I can relate. :-)
I'll try harder now ;-)
There exists a small gap between what was voted into the WP last week regarding move semantics, and what my intent is. Below I attempt to explain my intent, and where that differs from what is in the WP, I will attempt to fix with defect reports.
I haven't cached the differences between your intention and the WP. Are valid assignment/move-assignment for moved values required for types to be inserted in std containers?
[snip moving A to a container...]
The definition of "valid" is up to the author of A. At a minimum, generic code will need to destruct and/or assign "a" a new value. However the author of A is free to define as part of the class invariant:
After an A is moved from, you can't call A::bar()
I don't think that is a very good design myself. But I also don't think the standard should prohibit it.
I've been revising some classes which implement pseudo-move semantics in Interprocess and for unique resource owning classes (a shared memory object descriptor which is not copyable but movable) A::bar() would return error instead of crashing. Not a big advantage over crashing, but otherwise there is nothing to do, because the resource is unique. And these classes have the interesting "trivial destructor after move" trait, something we could not achieve with objects that still hold resources. It's a shame that the compiler can't automatically detect such classes to avoid calling destructors if they've been moved. std::vector would really appreciate it... ;-)
Originally I preferred vector move assignment as just swap(). However I've recently become convinced that this is not the best definition. I now prefer the semantics of clear(); swap();. This means that move assignment is O(N) instead of O(1). Ok Ion, take a deep breath and stay with me for just a minute longer. :-)
Holding my breath...
The semantics of ~thread() is going to be cancel(); detach();. That is, if thread A is holding a std::thread referencing thread B, and thread C cancels thread A, as thread A's stack unwinds, it will execute b.~thread() which cancels B, but does not wait around for B to respond to the cancel. Thus canceling A will not hang waiting on B to finish up.
[more good stuff]
I've been revising a bit the implementation of move semantics for Intrusive containers and I've also seen that there is no advantage if we are constructing a new value. However, my hope is that move semantics will be very successful and people will start using move semantics everywhere: class ObjectWithString { public: ///.. //We can assign from anything convertible to std::string template<class T> set_str(T &&convertible_to_str) { str_ = forward(str); } private: std::string str_; } ObjectWithString object_with_string; std::vector<ObjectWithString> object_vect; for(int i = 0; i < NUM; ++i){ str += "prefix_"; str.append(to_string(i)); str_vect[i].set_str(move(str)); } If target memory is deallocated with every move-assignment the complexity might be good in theory but the result is "my code calls delete/new every two steps". If strings are passed as lvalues, and the target has capacity, the copy assignment might be more efficient than the move assignment, because no memory is being deallocated/allocated (str is being reused in the loop). On the other hand, if memory is swapped to the outside world, we can avoid a lot of allocations, because str can reuse memory already present in the contained objects. Note that this is compatible with your clear() + swap() approach. If the standard does not guarantee this, my only hope is to use move construction hoping that the implementation optimizes this (although I bet that most implementation will swap the memory, specially if that's "extra officially" encouraged).
[vector thread example...]
I've just realized that I can end criticizing arguments that I've passionately defended before. In the GC debate I defended the need of deterministic destructors, something that I miss defending swap() as the implementation of the move assignment, because nobody really knows when the moved source is going to be destroyed. Your clear() + swap() proposal seems the answer. However, if the object to be move assigned is std::vector<std::string>, the call to destructors will deallocate all the strings. Maybe the implementations could just detect that vector is a container holding a value whose destructor has no "visible" side effects (well, deallocating memory: it's holding a container of values with trivial destructors). So move assignment for this: std::vector<std::vector<std::string> > can be quite heavy unless the implementation can detect that there is no need to call destructors. I imagine that unless the loop I've written or similar, the rvalue is going to be destroyed anyway, so maybe there is no big impact. Time will tell.
Getting back to shared_ptr move assignment, I would like to see the target's reference count decremented if it is not already at 0 prior to the move assignment (just as in copy assignment - this is the "clear" part of the algorithm). I would also like to not see any *new* constraints placed on the source as a result of being moved from. And finally I do not see the need to specify the value of the source after the move. I would like vendors to have the freedom to implement shared_ptr move assignment exactly as they do shared_ptr copy assignment. If atomic operations can be avoided (for example) by assuming that the source is a temporary (under move assignment) then so much the better.
shared_ptr can also optimize the trivial destructor case (or even std::vector<std::string> > case) but apart from avoiding the atomic operations it can also swap the heap allocated reference count (what happens with the deleter?) hoping someone can reuse it outside. After all, shared_ptr looks like a (special) container. Still breathless, Ion