Futures (pt2)¶

namespace bcc0 {

class task {
    // Need implementation here
public:
    task() = default;

    task(const task&) = delete;
    task(task&&) noexcept = default;

    task& operator=(const task&) = delete;
    task& operator=(task&&) noexcept = default;

    template <class F> // F model void()
    task(F f); // Need to implement

    void operator()(); // Need to implement
};

} // namespace bcc0
namespace bcc {

class task {
    struct concept;

    template <class F>
    struct model;

    std::unique_ptr<concept> _self;

public:
    task() = default;
    //...

    template <class F> // F model void()
    task(F f);         // Need to implement

    void operator()(); // Need to implement
};

} // namespace bcc
struct task::concept {
    virtual ~concept() {}
    virtual void invoke() = 0;
};
template <class F>
struct task::model final : concept {
    F _f;
    model(F f) : _f(move(f)) {}
    void invoke() override { _f(); }
};
template <class F>
task::task(F f) : _self(make_unique<model<F>>(move(f))) { }
namespace bcc {

void task::operator()() { _self->invoke(); }

} // namespace bcc
class sequential_process {
    // using task = function<void()>;
{
sequential_process process;

auto future = async_packaged(process, []{ return "Hello World!"s; });

cout << future.get() << endl;
}
  • You can only invoke get() on a future once
    • subsequent invocations will throw an exception!
{
sequential_process process;

auto future = async_packaged(process, []{ return "Hello World!"s; });

cout << future.get() << endl;
cout << future.get() << endl; // Will throw!
}
  • If you need to check the value multiple times
    • use std::shared_future<>
    • store the result in std::optional<> (C++17)
namespace v0 {

class interned_string {
    struct shared_pool {
        mutex _mutex;
        unordered_set<string> _pool;

        const string* insert(const string& a) {
            lock_guard<mutex> lock(_mutex);
            return &*_pool.insert(a).first;
        }
    };

    static auto pool() -> shared_pool& {
        static shared_pool result;
        return result;
    }

    const std::string* _string;

public:
    interned_string(const string& a) : _string(pool().insert(a)) {}
    const string& str() const { return *_string; }
};

} // namespace v0
struct shared_pool {
    unordered_set<string> _pool;
    sequential_process _process;

    auto insert(string a) -> future<const string*> {
        return async_packaged(_process, [this, _a = move(a)]() mutable {
            return &*_pool.insert(move(_a)).first;
        });
    }
};
namespace v1 {

class interned_string {
    // struct shared_pool

    static auto pool() -> shared_pool& {
        static shared_pool result;
        return result;
    }

    shared_future<const std::string*> _string;
public:
    interned_string(string a) : _string(pool().insert(move(a))) {}
    const string& str() const { return *_string.get(); }
};

} // namespace v1
  • std::future<> in C++11-17 is very limited

    • with no continuations they do not compose
      • there is no good way to extend interned_string::str() to return a future
    • the blocking behavior of get() means
      • we lose performance (Amdahl!)
      • inappropriate to use in a pooled scheduler
    • the only call get once behavior makes them cumbersome and error prone
      • converting to a shared_future imposes additional costs
  • Pros:

    • replace some common uses of condition variables
    • transformation from synchronous code to asynchronous code is simple
    • some parallelism is better than none

Homework¶

  • Read Future Ruminations