Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

This legendary C++ delegate article

http://www.codeproject.com/Articles/11015/The-Impossibly-Fast-C-Delegates

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;
}
share

locked by rolfl May 4 at 13:40

This question exists because it has historical significance, but it is not considered a good, on-topic question for this site, so please do not use it as evidence that you can ask similar questions here. This question and its answers are frozen and cannot be changed. More info: help center.

    
Is there any reason for using this instead of std::function? –  ony Jul 5 '13 at 11:51
    
@ony It is usually faster, especially when using pointer to member template parameters. –  user1095108 Jul 5 '13 at 12:32
    
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...); }); } –  ony Jul 5 '13 at 12:52
    
@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. –  user1095108 Jul 5 '13 at 13:24
    
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). –  miguel.martin Jul 17 '13 at 10:30
show 11 more comments

3 Answers 3

up vote 15 down vote accepted

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.

share
    
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? –  user1095108 Aug 17 '12 at 19:01
    
@user1095108: I thought the invocation was auto d = delegate::from_function<foo>(); -- there's no room for arguments, is there? –  Kerrek SB Aug 17 '12 at 19:11
    
What about lambdas? Can we turn them into delegates? –  Warren Seine Oct 30 '12 at 0:16
    
@WarrenSeine I've added lambda support. –  user1095108 Feb 16 '13 at 23:20
4  
@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. –  Ben Hymers Sep 4 '13 at 13:42
show 1 more comment

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.

share
    
I'll add your code to the delegate class, thanks for the contribution. –  user1095108 Sep 4 '13 at 12:47
    
Try the staticdelegate.hpp from the repository as well, please. –  user1095108 Sep 5 '13 at 23:38
    
@user1095108: pastebin.com/YwEaLhhF –  Vittorio Romeo Sep 6 '13 at 0:10
    
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). –  Vittorio Romeo Sep 6 '13 at 0:11
    
Note 3: I greatly cleaned up my Don's fastdelegate implementation, if you want to see how it handles raw global functions –  Vittorio Romeo Sep 6 '13 at 0:14
show 14 more comments

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?

share
    
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. –  user1095108 Sep 4 '13 at 13:12
    
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 –  Vittorio Romeo Sep 4 '13 at 13:15
    
Try to just assign in the loop: t2 = std::move(t2impl); t1 = std::move([&x, &t2](int i){ x = t2(i); };); –  user1095108 Sep 4 '13 at 13:18
    
@user1095108 no dice - results are same to the original benchmark code (delegate is a lot slower than std::function) –  Vittorio Romeo Sep 4 '13 at 13:21
    
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. –  user1095108 Sep 4 '13 at 13:27
show 10 more comments

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