shared_ptr, pimpl, reference semantics, and const correctness
I am continuing work on a futures library for boost. future objects (as currently considered by the C++ standards committee, in particular Peter Dimov's proposal) have reference semantics, like many pimpl types. I have some burning questions about const correctness. Hypothetical reference-semantic example: future f1; future f2 = f1; //f2 refers to the same underlying object as f1 f2.set(42); //do something assert(f1.get() == 42); // see, same underlying object This is roughly equivalent to: boost::shared_ptr<future_impl> f1; boost::shared_ptr<future_impl> f2 = f1; A std::vector<T>::iterator is an example of another type with reference semantics. So, say I have a simple future class: class future_impl { public: bool get() const {return value_;} void set(bool value) {value_ = value;} private: bool value_; }; class future { //not really a future, just example public: future() : impl_(new future_impl) {} future(const future &other) : impl_(other.impl_) {} bool get() const {return impl_->get();} // Set may not be const, but it does NOT really modify this class void set(bool value) {impl_->set(value);} private: boost::shared_ptr<future_impl> impl_; }; I have made the future::set() method non-const, but it does NOT really modify the underlying future class, only the future_impl class it refers to. This makes life very difficult when it comes to passing futures by reference: // alternative #1 - pass const future & void f1(const future &f) { bool v = f.get(); // no problem, get() is const f.set(!v); // cannot do this, set() is not const // BUT I can bypass the const-ness trivially, so it is pointless future f2(f); f2.set(!v); } // alternative #2 - pass future & // with #2, I can't do simple things like this: // f2(future(f)); //ERROR: non-const reference from a temporary - irritating void f2(future &f) { bool v = f.get(); // no problem, get() is const f.set(!v); } // alternative #3 - pass by value // Constructing shared_ptr's is VERY inefficient void f3(future f) { bool v = f.get(); // no problem, get() is const f.set(!v); // cannot do this, set() is not const } // alternative #4 - const_future base class with the subset of const methods? // kinda like a const_iterator? seems klunky. class const_future {bool get() const;}; class future : public const_future {void set(bool);} Does anyone have any advice or ideas? Oh, and then there is a possible need for something like a weak_ptr<future_impl>, like a weak_future class? And then a const_weak_future? I don't want to think about it... Thanks, Braddock Gaskill
Braddock Gaskill wrote:
// alternative #3 - pass by value // Constructing shared_ptr's is VERY inefficient void f3(future f) { bool v = f.get(); // no problem, get() is const f.set(!v); // cannot do this, set() is not const }
The construction of the shared_ptr (if not optimized out when the source is an rvalue) consists of one atomic increment. Its destruction at the end of f costs one atomic decrement and one memory barrier. In contrast, f.get() costs at best one atomic load and one memory barrier, and at worst blocks for an unspecified amount of time. f.set very likely costs one user-kernel transition (and a memory barrier), which is typically much more than a few atomic instructions. This of course doesn't address your const-correctness question, which disappears if you make f.set const. :-) In the next C++ we'll also have void f5( future && f ); as an option.
Hi Peter, On Wed, 01 Aug 2007 20:07:27 +0300, Peter Dimov wrote:
Braddock Gaskill wrote:
// alternative #3 - pass by value // Constructing shared_ptr's is VERY inefficient
The construction of the shared_ptr (if not optimized out when the source is an rvalue) consists of one atomic increment. Its destruction at the end of f costs one atomic decrement and one memory barrier.
I guess "VERY inefficient" is a relative term. I just spent a weak optimizing a DOM library where I attempted to use shared_ptr's in all element references to parse documents with millions of elements. Probably an inappropriate use from the start. As you point out, a future is not a flyweight class, so it may not be as critical.
This of course doesn't address your const-correctness question, which disappears if you make f.set const. :-)
Yes, I am considering that, but it seems semantically crooked. What would you recommend? I'm trying to stay on the same page as your N2185 futures proposal. Your proposal does not define set_value() as const, so users will presumably face the same issues. I can force users to pass by future value, but that doesn't feel like good practice and completely negates any const awareness anyway. Does "&&" remove this concern for N2185? (I'm not really familiar with the upcoming language features). Thanks, Braddock Gaskill
Braddock Gaskill wrote:
This of course doesn't address your const-correctness question, which disappears if you make f.set const. :-)
Yes, I am considering that, but it seems semantically crooked.
What would you recommend? I'm trying to stay on the same page as your N2185 futures proposal. Your proposal does not define set_value() as const, so users will presumably face the same issues.
Very good question. I personally pass shared_ptrs by value all the time without feeling any guilt so passing a future by value in the cases where I want to call non-const methods is good enough for me. On the other hand, is this an issue in a split future/promise model? Are you moving back to N2185-style "combined" future and if so, why? There's a discussion on the committee -lib reflector at the moment which has raised an interesting point: if no futures remain, it might be desirable (in some cases) to cancel the thread that is executing the task as nobody is listening for the result. This is the opposite of watching for the case where no promise is left and breaking the future::waits, and another possible argument in favor of a split future/promise model. The chances of N2185 being accepted in the standard at this point are approximately zero, so there's no need to stick to it except as a sanity check against design regressions. :-)
On Friday 10 August 2007 21:13, Peter Dimov wrote:
There's a discussion on the committee -lib reflector at the moment which has raised an interesting point: if no futures remain, it might be desirable (in some cases) to cancel the thread that is executing the task as nobody is listening for the result.
I personally like being able to "fire and forget" without having to keep the future around to prevent the asyncronous function call from cancelling itself. Especially in the case of a future<void> return value. -- Frank
participants (3)
-
Braddock Gaskill
-
Frank Mori Hess
-
Peter Dimov