I'm confused about the lifetime of parameters passed to C++ coroutines.
Answering to a previous question, smart people stated that
The lifetime of a parameter is [...] part of the caller's scope
Now, to follow up, what happens when passing default arguments like
generator my_coroutine(string&& s = string()) {...}
So, if my_coroutine was a normal function, s would be valid throughout its scope. However, this seems to no longer hold if my_coroutine is a coroutine.
In particular the results of the following coroutine-test surprised me:
#include <iostream>
#include <coroutine>
struct Test {
int i = 3;
Test() { std::cout << "test constructed\n";}
Test(const Test&) = delete;
Test(Test&&) = delete;
~Test() { std::cout << "test destructed\n"; }
friend std::ostream& operator<<(std::ostream& os, const Test& t) { return os << t.i; }
};
template<class T>
generator<int> coro_test(T&& t = T()) {
int i = 0;
while(i++ < 3) co_yield i;
if(i == t.i) co_yield 100;
}
int main () {
auto gen = coro_test<Test>();
while(gen.is_valid()) {
std::cout << *gen << "\n";
++gen;
}
return 0;
}
results:
test constructed
test destructed
1
2
3
PS: for completeness, here's my generator:
template<class T>
struct generator {
struct promise_type;
using coro_handle = std::coroutine_handle<promise_type>;
struct promise_type {
T current_value;
auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
auto initial_suspend() const noexcept { return std::suspend_never{}; }
auto final_suspend() const noexcept { return std::suspend_always{}; }
void unhandled_exception() const { std::terminate(); }
template<class Q>
auto yield_value(Q&& value) {
current_value = std::forward<Q>(value);
return std::suspend_always{};
}
};
private:
coro_handle coro;
generator(coro_handle h): coro(h) {}
public:
bool is_valid() const { return !coro.done(); }
generator& operator++() { if(is_valid()) coro.resume(); return *this; }
T& operator*() { return coro.promise().current_value; }
const T& operator*() const { return coro.promise().current_value; }
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
~generator() { if(coro) coro.destroy(); }
};
As pointed out in said "previous question", the first thing that happens in a coroutine is that parameters are "copied" into storage owned by the coroutine. However, the "copy" is ultimately initialized based on the type declared in the signature. That is, if a parameter is a reference, then the "copy" of that parameter is also a reference.
So a coroutine function that takes reference parameters is much like any kind of asynchronous function that takes reference parameters: the caller must ensure that the referenced object continues to exist throughout the time that the object will be used. A default parameter which initializes a reference is a circumstance that the caller cannot control the lifetime of (other than providing an explicit parameter).
You created an API that is inherently broken. Don't do that. Indeed, it's best to avoid passing references to async functions of any kind, but if you do, never give them default parameters.
Related
I've implemented a thread-safe wrapper for arbitrary objects that uses the drill down behavior of operator->(), my problem is that I don't quite understand how constness of the wrapped object is propagated through the calls to the operator->() down to the wrapped object pointer:
const T *operator->() const
{
mtx_.lock();
return const_cast<const T *>(t_);
}
is never called in the Protector class, even though ThreadSafe<const std::string> won't call a non-const method on the string it wraps, declaring it as const ThreadSafe<const std::string> changes nothing.
Could you explain why this is the case - the non-const version of the operator is called, but the end result is as if the const version of the operator was called.
#include <algorithm>
#include <boost/type_index.hpp>
#include <cassert>
#include <future>
#include <iostream>
#include <vector>
template <typename T>
class ThreadSafe
{
public:
template <class... Args>
explicit ThreadSafe(Args &&...args)
: obj_{std::forward<Args>(args)...}
{
}
auto operator->()
{
return Protector(&obj_, mtx_);
}
auto operator->() const
{
return Protector(std::add_const_t<T *>(&obj_), mtx_);
}
// method strictly for debugging
T copy() const
{
std::lock_guard<std::mutex> lck(mtx_);
return obj_;
}
private:
struct Protector
{
explicit Protector(T *t, std::mutex &mtx)
: mtx_(mtx)
, t_(t)
{
}
T *operator->()
{
mtx_.lock();
return t_;
}
const T *operator->() const
{
mtx_.lock();
return const_cast<const T *>(t_);
}
~Protector()
{
mtx_.unlock();
}
std::mutex &mtx_;
T *t_;
};
mutable std::mutex mtx_;
T obj_;
};
int main()
{
// ThreadSafe<std::string> ts_i{"123456789"};
// ts_i->append("0");
// std::cout << ts_i->c_str() << std::endl;
const ThreadSafe<const std::string> cts_i{"asd123"};
// cts_i->append("10"); // non const methods are inaccessible with a ThreadSafe<const T> wrapper, but const methods are.
std::cout << cts_i->substr(3) << std::endl;
return 0;
}
Both of these return the same type (non-const Protector), so naturally chained operator->() is treated the same in both cases:
auto operator->()
{
return Protector(&obj_, mtx_);
}
auto operator->() const
{
return Protector(std::add_const_t<T *>(&obj_), mtx_);
}
The second one wouldn't even compile if T is not const-qualified already.
I've created a generator that will have an overload operator* in order to be converted into std::ranges::subrange and I also want to overload yield_value from promise_type that accepts a subrange type that will be yielded recursively.
Source Code:
template <typename T>
class [[nodiscard]] generator {
public:
using value_type = T;
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
private:
handle_type handle_ { nullptr };
explicit generator(handle_type handle) : handle_(handle) {}
public:
struct promise_type {
value_type value_;
generator<value_type> get_return_object() {
return generator{ handle_type::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(const value_type& value) noexcept {
value_ = value;
return {};
}
template <typename U>
std::suspend_never await_transform(U&&) = delete;
void return_void() {}
};
generator() noexcept = default;
generator(const generator&) = delete;
generator(generator&& other) noexcept
: handle_(std::move(other.handle_)) {
other.handle_ = nullptr;
}
~generator() { if (handle_) handle_.destroy(); }
generator& operator=(const generator&) = delete;
generator& operator=(generator&& other) noexcept {
handle_ = std::move(other.handle_);
other.handle_ = nullptr;
return *this;
}
void swap(generator& other) noexcept {
using std::swap;
swap(handle_, other.handle_);
}
class iterator {
private:
handle_type handle_;
friend generator;
explicit iterator(handle_type handle) noexcept
: handle_(handle) {}
public:
using value_type = std::remove_cvref_t<T>;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = value_type*;
using const_pointer = const value_type*;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
iterator() noexcept = default;
friend bool operator==(const iterator& iter, std::default_sentinel_t) noexcept {
return iter.handle_.done();
}
friend bool operator==(std::default_sentinel_t s, const iterator& iter) noexcept {
return (iter == s);
}
iterator& operator++() {
if (handle_.done()) handle_.promise().unhandled_exception();
handle_.resume();
return *this;
}
iterator operator++(int) {
auto temp = *this;
++*this;
return temp;
}
reference operator*() noexcept {
return handle_.promise().value_;
}
pointer operator->() noexcept {
return std::addressof(operator*());
}
};
iterator begin() noexcept {
if (handle_) {
handle_.resume();
if (handle_.done())
handle_.promise().unhandled_exception();
}
return iterator{handle_};
}
std::default_sentinel_t end() noexcept {
return std::default_sentinel;
}
};
Example:
auto generate_0(int n) -> generator<int> {
while (n != 0)
co_yield n--;
}
auto generate_1() -> generator<int> {
for (const auto& elem : generate_0(10)) {
co_yield elem;
}
}
generate_1 will work obviously but I want have the same output like the generate_1 that each element is co_yield-ed directly inside the yield_value:
auto generate_1() -> generator<int> {
co_yield* generate_0(10);
}
Such that:
In class generator:
auto operator*() {
return std::ranges::subrange(begin(), end());
}
In nested class generator<...>::promise_type:
template <typename U>
std::suspend_always yield_value(const std::ranges::subrange<U, std::default_sentinel_t>& r) noexcept {
/** ... **/
return {};
}
First things first: bugs/odd bits on your end.
I don't think it's worth it trying to support old-style iterators. It doesn't make sense to default-construct generator<T>::iterator, and the new-style iterator concepts do not require it. You can tear out a lot of junk from iterator.
Also, == is magical. If x == y doesn't find a matching operator== but y == x does, then x == y is automatically rewritten to y == x. You don't need to provide both operator==s.
The promise_type does not need to hold T by value. An odd thing about yielding things from coroutines is that if you make yield_value take by-reference, you can get a reference to something that lives in the coroutine state. But the coroutine state is preserved until you resume it! So promise_type can instead hold T const*. Now you no longer require annoying things like copyability and default-constructibility from T.
It appears to be unnatural for a generator to initially suspend. Currently, if you do g.begin(); g.begin();, you will advance the generator even though you've incremented no iterator. If you make g.begin() not resume the coroutine and remove the initital suspension, everything just works. Alternatively, you could make generator track whether it has started the coroutine and only advance it to the first yield on begin(), but that's complicated.
While calling std::terminate() on every operation that's normally UB may be nice, it's also noisy and I'm just not going to include it in this answer. Also, please don't call it via unhandled_exception. That's just confusing: unhandled_exception has one very specific purpose and meaning and you are just not respecting that.
generator<T>::operator=(generator&&) leaks *this's coroutine state! Also, your swap is nonstandard because it is not a free 2-arg function. We can fix these by making operator= do what swap did and then getting rid of swap because std::swap works.
From a design/theory standpoint, I think it makes more sense to implement this syntax instead.
auto generate_1() -> generator<int> {
co_await generate_0(10);
}
A generator can temporarily give up control to another and may resume running after it awaits for the inner generator to run out. Implementing something to yield from a range can be easily implemented atop this by making a generator wrapping the range. This also lines up with the syntax in other languages like Haskell.
Now, coroutines have no stack. That means that as soon as we cross a function call boundary away from a coroutine like generate_1, it is not possible to suspend/resume that function via the coroutine state associated with the caller. So we have to implement our own stack, where we extend our coroutine state (promise_type) with the ability to record that it is currently pulling from another coroutine instead of having its own value. (Please note this would also apply to yielding from a range: whatever function is called to receive the range from generator_1 will not be able to control generator_1's coroutine.) We do this by making promise_type hold a
std::variant<T const*, std::subrange<iterator, std::default_sentinel_t>> value;
Note that promise_type does not own the generator represented by the subrange. Most of the time (as it is in generator_1) the same trick as yield_value applies: the generator which owns the sub-coroutine's state lives inside the caller coroutine's stack.
(This is also a point against directly implementing co_yield from a range: we need to fix the type of whatever is going into promise_type. From an API standpoint, it's understandable for co_await inside a generator<T> to accept generator<T>s. But if we implemented co_yield we'd only be able to directly handle one specific kind of rangeāa subrange wrapping a generator. That'd be weird. And to do otherwise we'd need to implement type-erasure; but the most obvious way to type-erase a range in this context is to make a generator. So we're back to a generator awaiting on another as being the more fundamental operation.)
The stack of running generators is now a linked-list threaded through their promise_types. Everything else just writes itself.
struct suspend_maybe { // just a general-purpose helper
bool ready;
explicit suspend_maybe(bool ready) : ready(ready) { }
bool await_ready() const noexcept { return ready; }
void await_suspend(std::coroutine_handle<>) const noexcept { }
void await_resume() const noexcept { }
};
template<typename T>
class [[nodiscard]] generator {
public:
struct iterator;
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
using range_type = std::ranges::subrange<iterator, std::default_sentinel_t>;
private:
handle_type handle;
explicit generator(handle_type handle) : handle(std::move(handle)) { }
public:
class iterator {
private:
handle_type handle;
friend generator;
explicit iterator(handle_type handle) noexcept : handle(handle) { }
public:
// less clutter
using iterator_concept = std::input_iterator_tag;
using value_type = std::remove_cvref_t<T>;
using difference_type = std::ptrdiff_t;
// just need the one
bool operator==(std::default_sentinel_t) const noexcept {
return handle.done();
}
// need to muck around inside promise_type for this, so the definition is pulled out to break the cycle
inline iterator &operator++();
void operator++(int) { operator++(); }
// again, need to see into promise_type
inline T const *operator->() const noexcept;
T const &operator*() const noexcept {
return *operator->();
}
};
iterator begin() noexcept {
return iterator{handle};
}
std::default_sentinel_t end() const noexcept {
return std::default_sentinel;
}
struct promise_type {
// invariant: whenever the coroutine is non-finally suspended, this is nonempty
// either the T const* is nonnull or the range_type is nonempty
// note that neither of these own the data (T object or generator)
// the coroutine's suspended state is often the actual owner
std::variant<T const*, range_type> value = nullptr;
generator get_return_object() {
return generator(handle_type::from_promise(*this));
}
// initially suspending does not play nice with the conventional asymmetry between begin() and end()
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(T const &x) noexcept {
value = std::addressof(x);
return {};
}
suspend_maybe await_transform(generator &&source) noexcept {
range_type range(source);
value = range;
return suspend_maybe(range.empty());
}
void return_void() { }
};
generator(generator const&) = delete;
generator(generator &&other) noexcept : handle(std::move(other.handle)) {
other.handle = nullptr;
}
~generator() { if(handle) handle.destroy(); }
generator& operator=(generator const&) = delete;
generator& operator=(generator &&other) noexcept {
// idiom: implementing assignment by swapping means the impending destruction/reuse of other implicitly handles cleanup of the resource being thrown away (which originated in *this)
std::swap(handle, other.handle);
return *this;
}
};
// these are both recursive because I can't be bothered otherwise
// feel free to change that if it actually bites
template<typename T>
inline auto generator<T>::iterator::operator++() -> iterator& {
struct visitor {
handle_type handle;
void operator()(T const*) { handle(); }
void operator()(range_type &r) {
if(r.advance(1).empty()) handle();
}
};
std::visit(visitor(handle), handle.promise().value);
return *this;
}
template<typename T>
inline auto generator<T>::iterator::operator->() const noexcept -> T const* {
struct visitor {
T const *operator()(T const *x) { return x; }
T const *operator()(range_type &r) {
return r.begin().operator->();
}
};
return std::visit(visitor(), handle.promise().value);
}
Nothing appears to be on fire.
static_assert(std::ranges::input_range<generator<unsigned>>); // you really don't need all that junk in iterator!
generator<unsigned> generate_0(unsigned n) {
while(n != 0) co_yield n--;
}
generator<unsigned> generate_1(unsigned n) {
co_yield 0;
co_await generate_0(n);
co_yield 0;
}
int main() {
auto g = generate_1(5);
for(auto i : g) std::cout << i << "\n"; // 0 5 4 3 2 1 0 as expected
// even better, asan is happy!
}
If you want to yield values from an arbitrary range, I would just implement this type-eraser.
auto generate_all(std::ranges::input_range auto &&r) -> generator<std::ranges::range_value_t<decltype(r)>> {
for(auto &&x : std::forward<decltype(r)>(r)) co_yield std::forward<decltype(x)>(x);
}
So you get e.g.
generator<unsigned> generate_1(unsigned n) {
co_await generate_all(std::array{41u, 42u, 43u});
co_await generate_0(n);
co_yield 0;
}
Suppose I have a class Option:
template<typename T>
class Option {
public:
Option() noexcept
{}
Option(T val) noexcept : val_(std::make_shared<T>(std::move(val)))
{}
const T & get() const
{
if (val_ == nullptr) {
throw std::out_of_range("get on empty Option");
}
return *val_;
}
const T & getOrElse(const T &x) const
{
return val_ == nullptr ? x : *val_;
}
private:
std::shared_ptr<T> val_;
};
The argument passed to Option::getOrElse is the default value to return when this Option is empty:
Option<int> x; // empty
int y = 123;
x.getOrElse(y); // == 123
However, I think the following code is not safe:
Option<int> x;
x.getOrElse(123); // reference to temporary variable!
A safer way would be to return by value from Option::getOrElse, but that would be wasteful when the Option is non-empty. Can I work around this somehow?
UPDATE: I'm thinking about perhaps overloading on the argument type (lvalue/rvalue) of getOrElse, but haven't figured out exactly how to do so.
UPDATE 2: Maybe this?
T getOrElse(T &&x) const { ... }
const T & getOrElse(const T &x) const { ... }
But I think this might be ambiguous because both lvalue and rvalue arguments fit the second version.
However, I think the following code is not safe:
Option<int> x;
x.getOrElse(123); // reference to temporary variable!
You are correct. This is why std::optional::value_or() returns a T and not a T& or T const&. As per the rationale in N3672:
It has been argued that the function should return by constant reference rather than value, which would avoid copy overhead in certain situations:
void observe(const X& x);
optional<X> ox { /* ... */ };
observe( ox.value_or(X{args}) ); // unnecessary copy
However, the benefit of the function value_or is only visible when the optional object is provided as a temporary (without the name); otherwise, a ternary operator is equally useful:
optional<X> ox { /* ... */ };
observe(ox ? *ok : X{args}); // no copy
Also, returning by reference would be likely to render a dangling reference, in case the optional object is disengaged, because the second argument is typically a temporary:
optional<X> ox {nullopt};
auto&& x = ox.value_or(X{args});
cout << x; // x is dangling!
I suggest you follow the same guidelines. If you really need to avoid the copy, use a ternary. This is safe and copyless:
Optional<int> ox = ...;
const int& val = ox ? *ox : 123;
If you really don't, or the Optional is an rvalue anyway, getOrElse() is more concise.
Since users of your class can expect the reference returned from Option::get() to be valid only as along as the the particular instance of the Option object's lifetime, you could reasonably make the same expectation for what is returned from Option::getOrElse().
In that case it might be an acceptable overhead for the object to maintain a collection of things that it needs to keep alive for the client:
#include <list>
#include <memory>
#include <iostream>
template<typename T>
class Option {
public:
Option() noexcept
{}
Option(T val) noexcept : val_(std::make_shared<T>(std::move(val)))
{}
const T & get() const
{
if (val_ == nullptr) {
throw std::out_of_range("get on empty Option");
}
return *val_;
}
const T & getOrElse(const T &x) const
{
if (val_ == nullptr) {
std::cout << "storing const T &\n";
elses_.push_front(x);
return elses_.front();
}
return *val_;
}
const T & getOrElse(T &&x) const
{
if (val_ == nullptr) {
std::cout << "storing T && by move\n";
elses_.push_front(std::move(x));
return elses_.front();
}
return *val_;
}
private:
std::shared_ptr<T> val_;
mutable std::list<T> elses_;
};
int main()
{
Option<int> x; // empty
int y = 123;
auto rx = x.getOrElse(y); // == 123
auto & rxx = x.getOrElse(42);
std::cout << "rx = " << rx << "\n";
std::cout << "rxx = " << rxx << "\n";
}
The references returned by Option::getOrElse() will be valid for as long as the reference returned from Option::get() would be. Of course, this also means that Option::getOrElse() can throw an exception.
As a small improvement, if the T type can be used as keys for an associative container you could use one of those instead of a std::list and easily avoid storing duplicates.
I'd rather return by reference and let the caller decide, whether he wants to store a reference to or a copy of the returned value.
Can I suggest to re-design this class?
It has a default ctor which can leave the val_ to be nullptr, but it has a get() at the same time which may throw exception because of dereference (*). It also designed to save T in shared_prt but return it as reference.
Let the client to know it's null:
template<typename T>
class Option {
public:
Option() noexcept
{}
Option(T val) noexcept : val_(std::make_shared<T>(std::move(val)))
{}
const T & get() const
{
return *val_;
}
bool IsNull() const
{
return val_ == nullptr;
}
private:
std::shared_ptr<T> val_;
};
The client code changed from:
Option option;
const T & ref = option.getOrElse(123);
to be:
Option option;
const T & ref = option.IsNull() ? 123 : option.get();
Why I delete the: if (val_ == nullptr) {
Let's make make_shared<> clear:
return a valid pointer, or
throw bad_alloc exception; it does not return null
So IsNull() is also useless, it should be like:
template<typename T>
class Option {
public:
Option(T val) noexcept : val_(std::make_shared<T>(std::move(val)))
{}
const T & get() const
{
return *val_;
}
private:
std::shared_ptr<T> val_;
};
Why to use shared_ptr? option objects can be move or copied several times? or else I prefer to design it like:
template<typename T>
class Option {
public:
Option(T val) noexcept : val_(std::move(val))
{}
const T & get() const
{
return val_;
}
private:
T val_;
};
Here is the problem I was thinking about lately. Let's say our interface is a member function that returns object which is expensive to copy and cheap to move (std::string, std::vector, et cetera). Some implementations may compute the result and return a temporary object while others may simply return a member object.
Sample code to illustrate:
// assume the interface is: Vec foo() const
// Vec is cheap to move but expensive to copy
struct RetMember {
Vec foo() const { return m_data; }
Vec m_data;
// some other code
}
struct RetLocal {
Vec foo() const {
Vec local = /*some computation*/;
return local;
}
};
There are also various "clients". Some only read the data, some require an ownership.
void only_reads(const Vec&) { /* some code */ }
void requires_ownership(Vec) { /* some code */ }
Code above composes well, but is not as efficient as it could be. Here are all combinations:
RetMember retmem;
RetLocal retloc;
only_reads(retmem.foo()); // unnecessary copy, bad
only_reads(retloc.foo()); // no copy, good
requires_ownership(retmem.foo()); // copy, good
requires_ownership(retloc.foo()); // no copy, good
What is a good way to fix this situation?
I came up with two ways, but I'm sure there is a better solution.
In my first attempt I wrote a DelayedCopy wrapper that holds either a value of T or a pointer to const T. It is very ugly, requires extra effort, introduces redundant moves, gets in the way of copy elision and probably has many other problems.
My second thought was a continuation-passing style, which works quite well but turns member functions into member function templates. I know, there is std::function, but it has its overhead so performance-wise it may be unacceptable.
Sample code:
#include <boost/variant/variant.hpp>
#include <cstdio>
#include <iostream>
#include <type_traits>
struct Noisy {
Noisy() = default;
Noisy(const Noisy &) { std::puts("Noisy: copy ctor"); }
Noisy(Noisy &&) { std::puts("Noisy: move ctor"); }
Noisy &operator=(const Noisy &) {
std::puts("Noisy: copy assign");
return *this;
}
Noisy &operator=(Noisy &&) {
std::puts("Noisy: move assign");
return *this;
}
};
template <typename T> struct Borrowed {
explicit Borrowed(const T *ptr) : data_(ptr) {}
const T *get() const { return data_; }
private:
const T *data_;
};
template <typename T> struct DelayedCopy {
private:
using Ptr = Borrowed<T>;
boost::variant<Ptr, T> data_;
static_assert(std::is_move_constructible<T>::value, "");
static_assert(std::is_copy_constructible<T>::value, "");
public:
DelayedCopy() = delete;
DelayedCopy(const DelayedCopy &) = delete;
DelayedCopy &operator=(const DelayedCopy &) = delete;
DelayedCopy(DelayedCopy &&) = default;
DelayedCopy &operator=(DelayedCopy &&) = default;
DelayedCopy(T &&value) : data_(std::move(value)) {}
DelayedCopy(const T &cref) : data_(Borrowed<T>(&cref)) {}
const T &ref() const { return boost::apply_visitor(RefVisitor(), data_); }
friend T take_ownership(DelayedCopy &&cow) {
return boost::apply_visitor(TakeOwnershipVisitor(), cow.data_);
}
private:
struct RefVisitor : public boost::static_visitor<const T &> {
const T &operator()(Borrowed<T> ptr) const { return *ptr.get(); }
const T &operator()(const T &ref) const { return ref; }
};
struct TakeOwnershipVisitor : public boost::static_visitor<T> {
T operator()(Borrowed<T> ptr) const { return T(*ptr.get()); }
T operator()(T &ref) const { return T(std::move(ref)); }
};
};
struct Bar {
Noisy data_;
auto fl() -> DelayedCopy<Noisy> { return Noisy(); }
auto fm() -> DelayedCopy<Noisy> { return data_; }
template <typename Fn> void cpsl(Fn fn) { fn(Noisy()); }
template <typename Fn> void cpsm(Fn fn) { fn(data_); }
};
static void client_observes(const Noisy &) { std::puts(__func__); }
static void client_requires_ownership(Noisy) { std::puts(__func__); }
int main() {
Bar a;
std::puts("DelayedCopy:");
auto afl = a.fl();
auto afm = a.fm();
client_observes(afl.ref());
client_observes(afm.ref());
client_requires_ownership(take_ownership(a.fl()));
client_requires_ownership(take_ownership(a.fm()));
std::puts("\nCPS:");
a.cpsl(client_observes);
a.cpsm(client_observes);
a.cpsl(client_requires_ownership);
a.cpsm(client_requires_ownership);
}
Output:
DelayedCopy:
Noisy: move ctor
client_observes
client_observes
Noisy: move ctor
Noisy: move ctor
client_requires_ownership
Noisy: copy ctor
client_requires_ownership
CPS:
client_observes
client_observes
client_requires_ownership
Noisy: copy ctor
client_requires_ownership
Are there better techniques to pass values that avoid extra copies yet are still general (allow returning both temporaries and data members)?
On a side note: the code was compiled with g++ 5.2 and clang 3.7 in C++11. In C++14 and C++1z DelayedCopy doesn't compile and I'm not sure whether it's my fault or not.
There are probably thousands of 'correct' ways. I would favour one in which:
the the method that delivers the reference or moved object is explicitly stated so no-one is in any doubt.
as little code to maintain as possible.
all code combination compile and do sensible things.
something like this (contrived) example:
#include <iostream>
#include <string>
#include <boost/optional.hpp>
// an object that produces (for example) strings
struct universal_producer
{
void produce(std::string s)
{
_current = std::move(s);
// perhaps signal clients that there is something to take here?
}
// allows a consumer to see the string but does not relinquish ownership
const std::string& peek() const {
// will throw an exception if there is nothing to take
return _current.value();
}
// removes the string from the producer and hands it to the consumer
std::string take() // not const
{
std::string result = std::move(_current.value());
_current = boost::none;
return result;
}
boost::optional<std::string> _current;
};
using namespace std;
// prints a string by reference
void say_reference(const std::string& s)
{
cout << s << endl;
}
// prints a string after taking ownership or a copy depending on the call context
void say_copy(std::string s)
{
cout << s << endl;
}
auto main() -> int
{
universal_producer producer;
producer.produce("Hello, World!");
// print by reference
say_reference(producer.peek());
// print a copy but don't take ownership
say_copy(producer.peek());
// take ownership and print
say_copy(producer.take());
// producer now has no string. next peek or take will cause an exception
try {
say_reference(producer.peek());
}
catch(const std::exception& e)
{
cout << "exception: " << e.what() << endl;
}
return 0;
}
expected output:
Hello, World!
Hello, World!
Hello, World!
exception: Attempted to access the value of an uninitialized optional object.
The following compiles under VS2010 (Express) but not gcc (4.6.2 here).
Lockable.h:
#include <boost/thread/mutex.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
template<typename T>
class LockedProxy : boost::noncopyable
{
public:
inline LockedProxy(boost::mutex & m, T * obj)
:lock(m),
t(obj)
{}
inline LockedProxy(const LockedProxy && other)
:lock(std::move(other.lock)),
t(std::move(other.t))
{}
inline T * operator->() { return t; }
inline const T * operator->() const { return t; }
inline const T & operator*() const { return *t; }
inline T & operator*() { return *t; }
private:
boost::interprocess::scoped_lock<boost::mutex> lock;
T * t;
};
template<typename T>
class Lockable
{
public:
// Convenience typefed for subclasses to use
typedef T LockableObjectType;
inline Lockable(const T & t)
:lockableObject(t)
{}
inline LockedProxy<LockableObjectType> GetLockedProxy() {
return LockedProxy<LockableObjectType>(mutex, &lockableObject);
}
protected:
LockableObjectType lockableObject;
boost::mutex mutex;
};
main.cpp:
#include <iostream>
#include <string.h>
#include "Lockable.h"
void f(Lockable<std::string> & str)
{
auto proxy = str.GetLockedProxy();
*proxy = "aa";
proxy->append("bb");
std::cout << "str = " << *proxy << std::endl;
}
void g(Lockable<int> & i)
{
{ // reduce lock's lifespan
auto proxy = i.GetLockedProxy();
*proxy = 321;
}
// relock, lock lives for the statement
std::cout << "i = " << *i.GetLockedProxy() << std::endl;
}
int main()
{
Lockable<std::string> str("abc");
//Can't use str here, it is not locked
f(str);
Lockable<int> i(123);
g(i);
return 0;
}
The errors:
In file included from main.cpp:3:0:
C:/Users/DrGibbs/Documents/code/boost_1_46_0/boost/interprocess/sync/scoped_lock.hpp: In constructor 'LockedProxy<T>::LockedProxy(const LockedProxy<T>&&) [with T = std::basic_string<char>, LockedProxy<T> = LockedProxy<std::basic_string<char> >]':main.cpp:7:37: instantiated from here
C:/Users/DrGibbs/Documents/code/boost_1_46_0/boost/interprocess/sync/scoped_lock.hpp:56:4: error: 'boost::interprocess::scoped_lock<Mutex>::scoped_lock(const boost::interprocess::scoped_lock<Mutex>&)[with Mutex = boost::mutex, boost::interprocess::scoped_lock<Mutex> = boost::interprocess::scoped_lock<boost::mutex>]' is privateLockable.h:14:29: error: within this context
C:/Users/DrGibbs/Documents/code/boost_1_46_0/boost/interprocess/sync/scoped_lock.hpp: In constructor 'LockedProxy<T>::LockedProxy(const LockedProxy<T>&&) [with T = int, LockedProxy<T> = LockedProxy<int>]':
main.cpp:18:36: instantiated from here
C:/Users/DrGibbs/Documents/code/boost_1_46_0/boost/interprocess/sync/scoped_lock.hpp:56:4: error: 'boost::interprocess::scoped_lock<Mutex>::scoped_lock(const boost::interprocess::scoped_lock<Mutex>&)[with Mutex = boost::mutex, boost::interprocess::scoped_lock<Mutex> = boost::interprocess::scoped_lock<boost::mutex>]' is private
Lockable.h:14:29: error: within this context
Well as far as I understand, in LockedProxy's move-constructor, the scoped_lock is not moved but copy-constructed, which really should not work. Shouldn't the std::move guanrantee its move-construction ? What am I missing ?
Your move constructor declares its parameter const:
inline LockedProxy(const LockedProxy && other)
It should be declared non-const:
inline LockedProxy(LockedProxy && other)
Your std::move(other.lock) is taking other.lock as a const reference, and so returning a const rvalue reference const boost::interprocess::scoped_lock<boost::mutex> &&, which cannot be passed to the move constructor of boost::interprocess::scoped_lock<boost::mutex>.
See also C++0x const RValue reference as function parameter, which explains that const rvalue references are almost entirely useless.