29
votes
\$\begingroup\$

This legendary C++ delegate article can be easily converted into C++11, without the need for fancy preprocessor magic in the original. I'd like to know if I got all the necessary C++11 nuances right. Suggestions?

#pragma once
#ifndef DELEGATE_HPP
# define DELEGATE_HPP

#include <cassert>

#include <memory>

#include <new>

#include <type_traits>

#include <utility>

template <typename T> class delegate;

template<class R, class ...A>
class delegate<R (A...)>
{
  using stub_ptr_type = R (*)(void*, A&&...);

  delegate(void* const o, stub_ptr_type const m) noexcept :
    object_ptr_(o),
    stub_ptr_(m)
  {
  }

public:
  delegate() = default;

  delegate(delegate const&) = default;

  delegate(delegate&&) = default;

  delegate(::std::nullptr_t const) noexcept : delegate() { }

  template <class C, typename =
    typename ::std::enable_if< ::std::is_class<C>{}>::type>
  explicit delegate(C const* const o) noexcept :
    object_ptr_(const_cast<C*>(o))
  {
  }

  template <class C, typename =
    typename ::std::enable_if< ::std::is_class<C>{}>::type>
  explicit delegate(C const& o) noexcept :
    object_ptr_(const_cast<C*>(&o))
  {
  }

  template <class C>
  delegate(C* const object_ptr, R (C::* const method_ptr)(A...))
  {
    *this = from(object_ptr, method_ptr);
  }

  template <class C>
  delegate(C* const object_ptr, R (C::* const method_ptr)(A...) const)
  {
    *this = from(object_ptr, method_ptr);
  }

  template <class C>
  delegate(C& object, R (C::* const method_ptr)(A...))
  {
    *this = from(object, method_ptr);
  }

  template <class C>
  delegate(C const& object, R (C::* const method_ptr)(A...) const)
  {
    *this = from(object, method_ptr);
  }

  template <
    typename T,
    typename = typename ::std::enable_if<
      !::std::is_same<delegate, typename ::std::decay<T>::type>{}
    >::type
  >
  delegate(T&& f) :
    store_(operator new(sizeof(typename ::std::decay<T>::type)),
      functor_deleter<typename ::std::decay<T>::type>),
    store_size_(sizeof(typename ::std::decay<T>::type))
  {
    using functor_type = typename ::std::decay<T>::type;

    new (store_.get()) functor_type(::std::forward<T>(f));

    object_ptr_ = store_.get();

    stub_ptr_ = functor_stub<functor_type>;

    deleter_ = deleter_stub<functor_type>;
  }

  delegate& operator=(delegate const&) = default;

  delegate& operator=(delegate&&) = default;

  template <class C>
  delegate& operator=(R (C::* const rhs)(A...))
  {
    return *this = from(static_cast<C*>(object_ptr_), rhs);
  }

  template <class C>
  delegate& operator=(R (C::* const rhs)(A...) const)
  {
    return *this = from(static_cast<C const*>(object_ptr_), rhs);
  }

  template <
    typename T,
    typename = typename ::std::enable_if<
      !::std::is_same<delegate, typename ::std::decay<T>::type>{}
    >::type
  >
  delegate& operator=(T&& f)
  {
    using functor_type = typename ::std::decay<T>::type;

    if ((sizeof(functor_type) > store_size_) || !store_.unique())
    {
      store_.reset(operator new(sizeof(functor_type)),
        functor_deleter<functor_type>);

      store_size_ = sizeof(functor_type);
    }
    else
    {
      deleter_(store_.get());
    }

    new (store_.get()) functor_type(::std::forward<T>(f));

    object_ptr_ = store_.get();

    stub_ptr_ = functor_stub<functor_type>;

    deleter_ = deleter_stub<functor_type>;

    return *this;
  }

  template <R (* const function_ptr)(A...)>
  static delegate from() noexcept
  {
    return { nullptr, function_stub<function_ptr> };
  }

  template <class C, R (C::* const method_ptr)(A...)>
  static delegate from(C* const object_ptr) noexcept
  {
    return { object_ptr, method_stub<C, method_ptr> };
  }

  template <class C, R (C::* const method_ptr)(A...) const>
  static delegate from(C const* const object_ptr) noexcept
  {
    return { const_cast<C*>(object_ptr), const_method_stub<C, method_ptr> };
  }

  template <class C, R (C::* const method_ptr)(A...)>
  static delegate from(C& object) noexcept
  {
    return { &object, method_stub<C, method_ptr> };
  }

  template <class C, R (C::* const method_ptr)(A...) const>
  static delegate from(C const& object) noexcept
  {
    return { const_cast<C*>(&object), const_method_stub<C, method_ptr> };
  }

  template <typename T>
  static delegate from(T&& f)
  {
    return ::std::forward<T>(f);
  }

  static delegate from(R (* const function_ptr)(A...))
  {
    return function_ptr;
  }

  template <class C>
  using member_pair =
    ::std::pair<C* const, R (C::* const)(A...)>;

  template <class C>
  using const_member_pair =
    ::std::pair<C const* const, R (C::* const)(A...) const>;

  template <class C>
  static delegate from(C* const object_ptr,
    R (C::* const method_ptr)(A...))
  {
    return member_pair<C>(object_ptr, method_ptr);
  }

  template <class C>
  static delegate from(C const* const object_ptr,
    R (C::* const method_ptr)(A...) const)
  {
    return const_member_pair<C>(object_ptr, method_ptr);
  }

  template <class C>
  static delegate from(C& object, R (C::* const method_ptr)(A...))
  {
    return member_pair<C>(&object, method_ptr);
  }

  template <class C>
  static delegate from(C const& object,
    R (C::* const method_ptr)(A...) const)
  {
    return const_member_pair<C>(&object, method_ptr);
  }

  void reset() { stub_ptr_ = nullptr; store_.reset(); }

  void reset_stub() noexcept { stub_ptr_ = nullptr; }

  void swap(delegate& other) noexcept { ::std::swap(*this, other); }

  bool operator==(delegate const& rhs) const noexcept
  {
    return (object_ptr_ == rhs.object_ptr_) && (stub_ptr_ == rhs.stub_ptr_);
  }

  bool operator!=(delegate const& rhs) const noexcept
  {
    return !operator==(rhs);
  }

  bool operator<(delegate const& rhs) const noexcept
  {
    return (object_ptr_ < rhs.object_ptr_) ||
      ((object_ptr_ == rhs.object_ptr_) && (stub_ptr_ < rhs.stub_ptr_));
  }

  bool operator==(::std::nullptr_t const) const noexcept
  {
    return !stub_ptr_;
  }

  bool operator!=(::std::nullptr_t const) const noexcept
  {
    return stub_ptr_;
  }

  explicit operator bool() const noexcept { return stub_ptr_; }

  R operator()(A... args) const
  {
//  assert(stub_ptr);
    return stub_ptr_(object_ptr_, ::std::forward<A>(args)...);
  }

private:
  friend struct ::std::hash<delegate>;

  using deleter_type = void (*)(void*);

  void* object_ptr_;
  stub_ptr_type stub_ptr_{};

  deleter_type deleter_;

  ::std::shared_ptr<void> store_;
  ::std::size_t store_size_;

  template <class T>
  static void functor_deleter(void* const p)
  {
    static_cast<T*>(p)->~T();

    operator delete(p);
  }

  template <class T>
  static void deleter_stub(void* const p)
  {
    static_cast<T*>(p)->~T();
  }

  template <R (*function_ptr)(A...)>
  static R function_stub(void* const, A&&... args)
  {
    return function_ptr(::std::forward<A>(args)...);
  }

  template <class C, R (C::*method_ptr)(A...)>
  static R method_stub(void* const object_ptr, A&&... args)
  {
    return (static_cast<C*>(object_ptr)->*method_ptr)(
      ::std::forward<A>(args)...);
  }

  template <class C, R (C::*method_ptr)(A...) const>
  static R const_method_stub(void* const object_ptr, A&&... args)
  {
    return (static_cast<C const*>(object_ptr)->*method_ptr)(
      ::std::forward<A>(args)...);
  }

  template <typename>
  struct is_member_pair : std::false_type { };

  template <class C>
  struct is_member_pair< ::std::pair<C* const,
    R (C::* const)(A...)> > : std::true_type
  {
  };

  template <typename>
  struct is_const_member_pair : std::false_type { };

  template <class C>
  struct is_const_member_pair< ::std::pair<C const* const,
    R (C::* const)(A...) const> > : std::true_type
  {
  };

  template <typename T>
  static typename ::std::enable_if<
    !(is_member_pair<T>{} ||
    is_const_member_pair<T>{}),
    R
  >::type
  functor_stub(void* const object_ptr, A&&... args)
  {
    return (*static_cast<T*>(object_ptr))(::std::forward<A>(args)...);
  }

  template <typename T>
  static typename ::std::enable_if<
    is_member_pair<T>{} ||
    is_const_member_pair<T>{},
    R
  >::type
  functor_stub(void* const object_ptr, A&&... args)
  {
    return (static_cast<T*>(object_ptr)->first->*
      static_cast<T*>(object_ptr)->second)(::std::forward<A>(args)...);
  }
};

namespace std
{
  template <typename R, typename ...A>
  struct hash<::delegate<R (A...)> >
  {
    size_t operator()(::delegate<R (A...)> const& d) const noexcept
    {
      auto const seed(hash<void*>()(d.object_ptr_));

      return hash<typename ::delegate<R (A...)>::stub_ptr_type>()(
        d.stub_ptr_) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    }
  };
}

#endif // DELEGATE_HPP

Example use:

#include <iostream>

#include "delegate.hpp"

struct A
{
  void foo(int a)
  {
    std::cout << "method got: " << a << std::endl;
  }
};

void foo(int a)
{
  std::cout << "function got: " << a << std::endl;
}

int main(int argc, char* argv[])
{
  auto d1(delegate<void (int)>::from<foo>());

  A a;
  auto d2(delegate<void (int)>::from<A, &A::foo>(&a));
  auto d3(delegate<void (int)>{foo});
  auto d4(delegate<void (int)>(&a, &A::foo));

  d1(1);
  d2(2);
  d3(3);
  d4(4);

  int b(2);

  auto dx(delegate<void ()>(
    [b](){std::cout << "hello world: " << b << std::endl;}));

  dx();

  return 0;
}
\$\endgroup\$
16
  • \$\begingroup\$ Is there any reason for using this instead of std::function? \$\endgroup\$
    – ony
    Jul 5, 2013 at 11:51
  • \$\begingroup\$ @ony It is usually faster, especially when using pointer to member template parameters. \$\endgroup\$ Jul 5, 2013 at 12:32
  • \$\begingroup\$ is there any reason to have slower implementation of std::function in C++ lib/run-time? As well there is code return from([object, method_ptr](A const... args){ return (object.*method_ptr)(args...); }); } \$\endgroup\$
    – ony
    Jul 5, 2013 at 12:52
  • \$\begingroup\$ @ony Benchmark and you'll see. If you like it, use it. Is there something wrong with the code? Does it fail to compile? If so, please post compile error. The code is C++11. You can also post a critique as an answer. \$\endgroup\$ Jul 5, 2013 at 13:24
  • \$\begingroup\$ Last time I checked std::function is quite fast compared to the impossibly fast delegate class. I did this benchmark by using my library awhile ago with Apple clang 4.1 (although the benchmark for std::function isn't actually there, I'm sure you could test it out yourself using your own code or even mine). \$\endgroup\$ Jul 17, 2013 at 10:30

3 Answers 3

15
votes
\$\begingroup\$

It looks by and large OK. Just some nitpicks

  • the default constructor could just be delegate() = default;, or otherwise initialize all members... or remove it entirely if it makes no sense.

  • the copy constructor should use initialization, not assignment.

  • swap should return void.

  • Assignment should pass-by-value:

    delegate& operator=(delegate rhs) { rhs.swap(*this); return *this; }
    
  • Invocation should use arbitrary arguments and forwarding:

    template <typename ...B>
    R operator()(B &&... b)
    {
        return (*stub_ptr)(object_ptr, std::forward<B>(b)...);
    }
    

    In fact, you should add enable_if with some variadic version of is_constructible<A, B>... to the operator so you don't create impossible overloads.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks for your comments! I think the forwarding is still shaky, as the static function stubs still make copies of their arguments. Can this be fixed? \$\endgroup\$ Aug 17, 2012 at 19:01
  • \$\begingroup\$ @user1095108: I thought the invocation was auto d = delegate::from_function<foo>(); -- there's no room for arguments, is there? \$\endgroup\$
    – Kerrek SB
    Aug 17, 2012 at 19:11
  • \$\begingroup\$ What about lambdas? Can we turn them into delegates? \$\endgroup\$ Oct 30, 2012 at 0:16
  • \$\begingroup\$ @WarrenSeine I've added lambda support. \$\endgroup\$ Feb 16, 2013 at 23:20
  • 4
    \$\begingroup\$ @doug65536 to avoid having to have const T& and T&& versions of assignment; passing by value will choose move construction or copy construction automatically for rhs. \$\endgroup\$
    – Ben Hymers
    Sep 4, 2013 at 13:42
4
votes
\$\begingroup\$

I love this! Ran some quick benchmarks:

int x{0}; 
for(int xx = 0; xx < 5; ++xx)
{
    startBenchmark();
    {
        std::function<int(int)> t2 = [&x](int i){ return i + x; };
        std::function<void(int)> t1 = [&x, &t2](int i){ x = t2(i); };
        for(int i = 0; i < 1000000000; ++i) t1(i);
    } lo << lt("std::func") << endBenchmark() << endl;

    startBenchmark();
    {
        delegate<int(int)> t2 = [&x](int i){ return i + x; };
        delegate<void(int)> t1 = [&x, &t2](int i){ x = t2(i); };
        for(int i = 0; i < 1000000000; ++i) t1(i);
    } lo << lt("ssvu::fastfunc") << endBenchmark() << endl;
}

Results:

[std::func]                           3278 ms
[ssvu::fastfunc]                      2147 ms
[std::func]                           3264 ms
[ssvu::fastfunc]                      2126 ms
[std::func]                           3302 ms
[ssvu::fastfunc]                      2182 ms
[std::func]                           3306 ms
[ssvu::fastfunc]                      2135 ms
...

Your delegate class is always faster by 1 seconds, even in this naive benchmark.


Actual review:

I tested changing all std::function<R(A...)> to your delegate<R(A...)> in all of my projects.

After making a small adjustment to the delegate code, everything works as expected!

In some of my code I used to do this:

void a(std::function<void()> mMaybe = nullptr)
{
    // do something
    if(mMaybe != nullptr) mMaybe();
}

This code broke when using delegate instead of std::function. I fixed it by adding these lines to your delegate class:

delegate(std::nullptr_t) : object_ptr_{nullptr}, stub_ptr_{nullptr} { }
bool operator==(std::nullptr_t) const noexcept      { return object_ptr_ == nullptr; }
bool operator!=(std::nullptr_t) const noexcept      { return object_ptr_ != nullptr; }

Now the above code compiles and works correctly. With this small adjustment, the delegate class is a probably faster drop-in replacement for std::function.

\$\endgroup\$
19
  • \$\begingroup\$ I'll add your code to the delegate class, thanks for the contribution. \$\endgroup\$ Sep 4, 2013 at 12:47
  • \$\begingroup\$ Try the staticdelegate.hpp from the repository as well, please. \$\endgroup\$ Sep 5, 2013 at 23:38
  • \$\begingroup\$ @user1095108: pastebin.com/YwEaLhhF \$\endgroup\$ Sep 6, 2013 at 0:10
  • \$\begingroup\$ Note: rawfunc is delegate<int(int&, int)> t1 = &rawFunc;, with rawFunc being a simple global function. Note 2: if you manage to optimize rawfunc, mem rawfunc can be optimized using the same concepts behind Don's fastdelegate (binding it as if it was a global function). \$\endgroup\$ Sep 6, 2013 at 0:11
  • \$\begingroup\$ Note 3: I greatly cleaned up my Don's fastdelegate implementation, if you want to see how it handles raw global functions \$\endgroup\$ Sep 6, 2013 at 0:14
0
votes
\$\begingroup\$

Problematic results:

[std::func]                           31855 ms
[ssvu::fastfunc]                      123848 ms

Benchmark code:

int x{0};
for(int xx = 0; xx < 5; ++xx)
{
    startBenchmark();
    {
        for(int i = 0; i < 1000000000; ++i)
        {
            std::function<int(int)> t2 = t2impl;
            std::function<void(int)> t1 = [&x, &t2](int i){ x = t2(i); };
            t1(i);
        }
    } lo << lt("std::func") << endBenchmark() << endl;
    startBenchmark();
    {
        for(int i = 0; i < 1000000000; ++i)
        {
            FastFunc<int(int)> t2 = t2impl;
            FastFunc<void(int)> t1 = [&x, &t2](int i){ x = t2(i); };
            t1(i);
        }
    } lo << lt("ssvu::fastfunc") << endBenchmark() << endl;
}

std::function performed way better on this benchmark. Any idea why?

\$\endgroup\$
15
  • \$\begingroup\$ It probably has to do something with initialization of functors. ::std::function supposedly optimizes initialization with small functors. I didn't optimize that use case. Try to move the definition of FastFunc objects outside of the loop, and then simply std::move the lambda object into it. \$\endgroup\$ Sep 4, 2013 at 13:12
  • \$\begingroup\$ I did some profiling with valgrind (only for the delegate class): it seems the real bottleneck is dynamic memory allocation i.imgur.com/k01y9fA.png \$\endgroup\$ Sep 4, 2013 at 13:15
  • \$\begingroup\$ Try to just assign in the loop: t2 = std::move(t2impl); t1 = std::move([&x, &t2](int i){ x = t2(i); };); \$\endgroup\$ Sep 4, 2013 at 13:18
  • \$\begingroup\$ @user1095108 no dice - results are same to the original benchmark code (delegate is a lot slower than std::function) \$\endgroup\$ Sep 4, 2013 at 13:21
  • \$\begingroup\$ If this is a frequent use-case in your code, you need to find some other delegate or optimize this code for it (i.e. instead of new-ing, you can use a small char[] array to store the functor - watch out for alignment issues) or you can try using Boost.Pool or some small object allocator. Note that the original delegate didn't support functors (lambda objects) at all, but exploited the possibility of compile-time pointer-to-function and pointer-to-function-member template parameters. \$\endgroup\$ Sep 4, 2013 at 13:27

Not the answer you're looking for? Browse other questions tagged or ask your own question.