5
\$\begingroup\$

I've been finding myself wanting the following pattern recently:

void perform_async_operation(std::function<void(int)>);

class some_class {
  public:
    void foo() {
      perform_async_operation([this](int result) {
        data_ = result;
      });
    }

  private:
    int data_;
};

Now, the issue here is that the lifetime of some_class if fully disconnected from when the callback will be invoked, and I do not want to move to shared ownership to put a shared_ptr in the lambda (like how the boost::ASIO examples handle that situation).

I do, however, have a saving grace: I know for certain that the callback will be invoked in the same thread that manages the lifetime of some_class.

In order to solve this, I've created a bastardized version of weak_ptr (with a matching equivalent of enabled_shared_from_this) that does not require the object to be managed by a shared_ptr in the first place.

#ifndef SLT_WEAK_REF_INCLUDED_H
#define SLT_WEAK_REF_INCLUDED_H

#include <memory>

template <typename T>
class weak_ref {
 public:
  weak_ref(std::shared_ptr<T*> weak_data) : weak_data_(std::move(weak_data)) {}

  operator bool() const { return *weak_data_ != nullptr; }
  T* operator->() const { return *weak_data_; }

 private:
  // Can be safely assumed to always be set.
  std::shared_ptr<T*> weak_data_;
};

template <typename CRTP>
class enable_weak_ref {
 public:
  enable_weak_ref() = default;
  enable_weak_ref(enable_weak_ref&& rhs)
      : weak_data_(std::move(rhs.weak_data_)) {
    if (weak_data_) {
      *weak_data_ = this;
    }
  }

  enable_weak_ref& operator=(enable_weak_ref&& rhs) {
    weak_data_ = std::move(rhs.weak_data_);
    if (weak_data_) {
      *weak_data_ = static_cast<CRTP*>(this);
    }
  }

  // explicitely do nothing
  enable_weak_ref(enable_weak_ref const& rhs) {}
  enable_weak_ref& operator=(enable_weak_ref const&) {}

  ~enable_weak_ref() {
    if (weak_data_) {
      *weak_data_ = nullptr;
    }
  }

  weak_ref<CRTP> get_weak_ref() {
    if (!weak_data_) {
      weak_data_ = std::make_shared<CRTP*>(static_cast<CRTP*>(this));
    }

    return weak_ref<CRTP>(weak_data_);
  }

 private:
  // TODO: perhaps switch to a simple manually ref-counted type here.
  // instead of the overkill shared_ptr<>
  std::shared_ptr<CRTP*> weak_data_;
};

#endif

sample usage:

#include "weak_ref.h"

#include <queue>
#include <iostream>
#include <functional>

std::queue<std::function<void(int)>> queue_;

void queue_delayed(std::function<void(int)> cb) {
  queue_.emplace(std::move(cb));
}

class my_class : public enable_weak_ref<my_class> {
public:
  my_class() {
    queue_delayed([ref = get_weak_ref()](int v){
      if(ref) {
        std::cout << "passed " << v << std::endl;
        ref->value = v;
      }
      else {
        std::cout << "saved " << v << std::endl;
      }
    });
  }

  int value = 0;
};

int main() {
  my_class a;
  queue_.front()(12);
  queue_.pop();
  {
    my_class b;
  }
  queue_.front()(5);
  queue_.pop();

  return 0;
}

Notes:

  • Moving an object in a lambda is a C++14 feature, without which, a lot of back-and-forth ref-counting happens.
  • As mentioned, this is intentionally not thread-safe. Thread safety can be added if anyone needs it eventually.

Issues:

  • Is there a nicer way to tackle this I'm overlooking here?
  • weak_ref is very weak name, I'd gladly welcome better suggestions.
  • As always, comments on general code quality are most desired.
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

I never saw a similar pattern so it was really interesting to understand your code !

First, about the name weak_ref, it implies that it has the syntax of a reference, but it is not the case (for instance, operator-> is for pointers). I personnaly named it something like ptr_to_instance.

Then, it seems the weak_ref should not be used on its own, without calling enable_weak_ref::get_weak_ref(). So I will make the weak_ref constructor private, making enable_weak_ref a friend. I will put the weak_ref class inside enable_weak_ref :

template<typename CRTP>
class enable_weak_ref {
  public:
    class weak_ref { /**/ };
    /**/
};

So instead of weak_ref<T> we have enable_weak_ref<T>::weak_ref. I think it makes more understandable the use of these classes.

Also, because weak_ref should not modify the pointer in weak_data_, I would use std::shared_ptr<T* const> so it is clear that this pointer is for read-only use.

If you do not want to use std::shared_ptr, you should create a int* (counting references), a CRTP** (which will hold this or nullptr) in enable_weak_ref, and weak_ref should have a int* const and a CRTP const* const (const specifier shows that weak_ref is linked to only one enable_weak_ref and can not be changed).

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the review! A few notes: both my problem statement and sample code showcase mutation, I'm not sure where you see the constness requirement. Namespacing weak_ref would just be super-rough api-wise, it's not always going to be inferred. Your alternative to shared_ptr leaves a lot to be desired, it would be much better to pack the ref count and data pointer in a struct. It's also not "that" simple, there's quite a few edge cases to handle. \$\endgroup\$
    – user128454
    Commented Jan 4, 2018 at 14:27
  • \$\begingroup\$ About alternative to shared_ptr, yes it was just "you may need" not a full implementation. I don't know very much about mutation, but constness as I depicted is not a requirement but just to explicit the uses of the variable. I saw I made a mistake, I didn't want to say shared_ptr<T const*> but shared_ptr<T* const>, because your weak_ptr should not modify which instance the pointer points. \$\endgroup\$ Commented Jan 4, 2018 at 14:50
  • \$\begingroup\$ But the variable is not const in any meaning of the word. The pointer can be changed (in enable_weak_ref's move constructor), and the content pointed to by the pointer can change (as seen in the examples) \$\endgroup\$
    – user128454
    Commented Jan 4, 2018 at 14:51
  • \$\begingroup\$ enable_weak_ref can change where the pointer points, so indeed it has a shared_ptr<T*>, but weak_ref needs only to know the value of the pointer itself, eventually modifying the dereference. but the pointer itself should not be modified in weak_ref, so it holds shared_ptr<T* const>. It is what i meant. \$\endgroup\$ Commented Jan 4, 2018 at 14:55
2
\$\begingroup\$
operator bool() const { return *weak_data_ != nullptr; }

Should be explicit operator bool() const { ... } to prevent accidental conversions.


Perhaps the weak_ref class could be moveable and / or copyable.


This might be a decent use-case for std::promise and std::future:

#include <future>

class foo
{
public:

    explicit foo(std::queue<std::function<void(int)>>& q)
    {
        auto p = std::make_shared<std::promise<int>>(); // :(
        value_ = p->get_future();
        q.emplace([p = std::move(p)] (int v) mutable { p->set_value(v); });
    }

    std::future<int> value_;
};

It's a bit ugly because std::function doesn't support move-only types - so we have to put the promise into a shared_ptr to make it "copyable".

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.