Click here to Skip to main content
15,886,724 members
Articles / Programming Languages / C++

A Smart Function Pointer

Rate me:
Please Sign up or sign in to vote.
4.44/5 (9 votes)
28 May 2015CPOL7 min read 11.1K   154   12   1
A smart function pointer

Introduction

C++ does not have a standard general purpose function pointer supporting equality comparisons that can target standalone functions, member functions, and lambda/function objects so long as they share a common function signature. I am proposing a type called fun_ptr that can do this. For the case of member function targets an optional object instance pointer may also be stored (see fun_ptr constructors). This is sometimes referred to as a delegate by other languages. Equality comparison would be supported and produce intuitive results. The only requirement for equality is user defined function objects must provide operator==. For all other targets (including lambda functions) equality works automatically. If equality is not required for a particular user defined function object defining operator== may be omitted.

Why not std::function + std::bind?

It is true the combination of std::function + std::bind is able to target standalone, member functions, and function objects. However std::function has constness problems (see N4159), lacks equality comparison because "it cannot be implemented "well"" (see boost faq) and has "loose" function signature matching. Equality comparison is a fundamental operation a smart function pointer should support. Herb Sutter points out in Generalizing Observer an issue using std::function to implement the observer pattern is the lack of equality comparison. Boost’s signals2 library overcomes this problem by returning a handle that can be used to detach at a later time. This works well when one object is responsible for both connecting and disconnecting. However, when connections and disconnections may occur in any number of locations the connection handle has to become a shared object. It would be more convenient if the disconnection could be performed simply by knowing the connection target. Additionally using std::function in combinations with std::bind produces rather ugly syntax.

fun_ptr improves upon std::function and std::bind by implementing equality comparisons, strictly enforcing function signature, and providing a more pleasant syntax. In addition make_fun helper functions simplify creation of fun_ptr objects (see examples).

Definitions

Since it is desirable to implement equality comparison it is important to know what equality means. John Lakos has a talk on the meaning of values (see you tube: Value Semantics: It ain't about the syntax). Basically a value type should have three properties:

  1. be substitutable with a copy
  2. If A and B have the same values and the same salient operations are performed on A and B then both objects will again have the same value.
  3. Two objects of a given value-semantic type have the same value if and only if there does not exist a distinguishing sequence among all of its salient operations.

The definition for a salient operation is basically any operation that has an effect on the type's value. The example Lakos gives is a std::vector's size is a salient attribute, but its capacity is not. Therefore resize(), push_back(), etc. are be salient operations but reserve() and shrink_to_fit() are not.

Smart Function Pointer Description

fun_ptr is a value type with its only salient attribute being its target. The target is the function (standalone, member, function object/lambda or nullptr) and object instance pointer when applicable. Unlike std::function fun_ptr is designed to have function pointer like semantics and support equality operations. The target is invoked via operator(). Invoking a fun_ptr with a nullptr target is not permitted. operator= changes the target of the fun_ptr and is the only salient mutating operation that can be performed on a fun_ptr object (note: it is important to note operator() is not a salient operation).

Equality comparison. fun_ptrs with targets that point to the same object instance (when applicable) and member function compare equal. Two fun_ptrs with different target types (i.e mutable member function and const member function) will compare unequal. Two fun_ptrs with different object instances will compare unequal. The equality comparison result is unspecified if the target type and object instances are the same but one or both of the member functions are virtual (because C++ spec does not specify the result of comparison of virtual function pointers). The equality comparison result is unspecified if a user defined function object does not implement operator==. A possible algorithm to determine equality is as follow:

  1. If the targets are different types return false
  2. Else if both targets are nullptr return true
  3. Else if both targets are standalone functions
    1. Return the comparison of the function pointers
  4. Else if both targets are member functions (without instance pointers)
    1. If member functions point to different classes return false
    2. Else return the comparison of the member function pointers
  5. Else if both targets are member function pointers /w instance pointers
    1. If targets are different classes return false
    2. Else if instance pointers are different return false
    3. Else return comparison of the function pointers (unspecified if at least one is virtual)
  6. Else if both targets are function objects with operator==
    1. Return result of operator==
  7. Else if both targets are function objects without operator==
    1. If function object types are different return false
    2. Else (function objects types are the same) return true

fun_ptr does not allow targeting mutating lambda functions because it would violate function pointer semantics and potentially cause thread safety issues (see N4159). C++ also guarantees each lambda function defined has a unique type. Combining these two items allows fun_ptr to handle lambda function equality correctly (step 7a and 7b). This works because type alone is enough to determine equality among non-mutating lambda functions.

Function object (or lambda) types are by convention (see Meyers Effective STL) passed by value (and often are temporary objects). When a fun_ptr is constructed with a function object it constructs a copy of the function object as a member of fun_ptr.

For member function targets it is up to the user to ensure the target references a valid object. That is invoking a fun_ptr with a target that no longer exists is not permitted. fun_ptr has shallow const and shallow copy semantics much like a function pointer.

The question might be asked: is fun_ptr thread safe? Recall that fun_ptr was designed to have function pointer like semantics so the answer is the same as the answer to the question: "is invoking a function pointer thread safe?" It depends on the implementation of the function its points to. For standalone functions the function would need to be reentrant. For a member function or function object it would need to be internally synchronized.

Smart Function Pointer Implementation

I have implemented two separate versions of fun_ptr. The first uses dynamic memory allocation and RTTI. The second uses the small object optimization for everything except large function objects and a template trick to remove RTTI. As a result the 2nd implementation it is on the order of 10 times faster for most operations (large function objects are an exception because they still require dynamic memory allocation to construct).

Performance. I included Don Clugston’s fastdelegate and another of my libraries (Delegate based on Elbert Mai’s lightweight Callbacks) for completeness of this performance test. Delegate is ultra-fast but has some undesirable characteristics: it uses macros for creation, doesn’t support function objects, and requires quirky syntax for dependent template function names. Don Clugston’s fastdelegate is another popular delegate that is also very fast but uses some nonstandard C++ and doesn’t support function objects. As pointed to by the invoke bench all the implementations have similar calling performance. It’s the creation, destruction, assignment, and equality check, and supported targets that separate them.

These tests were performed with gcc4.7.3.

  fastdelegate Delegate fun_ptr2 fun_ptr3 std::function+std::bind
Size(bytes) 12 8 12 to 16+ 20+ 16 + ???
Bench1(sec) 1.15 1.033 34.068 3.6 equality not supported
Bench2(sec) 1.142 1.016 24.318 3.21 25.998
Bench3(sec) 0.603 0.536 0.947 0.986 1.025
lambda no no yes yes yes
member function yes yes yes yes yes
non member function yes yes yes yes yes
Notes:plus indicates more for large functors
Bench1: create, destroy, assign, equality check, invoke
Bench2: create, destroy, assign, invoke
Bench3: invoke only

Observer patter / Events

See Generalizing Observer by Herb Sutter for a more thorough discussion of the observer pattern and problems with using std::function.

With fun_ptr and the use of its operator== the observer pattern can be implemented without having to resort to cookies or handles to detach observers. The code below is real and works. In my opinion this code has syntax on par with C# native implementations of delegates and events.

C#
class High_score
{
public:
   //a public event object
   event<void(int)> newHighScoreEvent;
 
   High_score()
      : newHighScoreEvent()
      , newHighScoreEventInvoker(newHighScoreEvent.get_invoker())
      , high_score(0)
   {
   }
 
   void submit_score(int score)
   {
      if (score > high_score) {
         high_score = score;
         newHighScoreEventInvoker(high_score); //invoke the event
      }
   }
 
private:
   //a private invoker for the event (only this class may invoke the event)
   decltype(newHighScoreEvent)::invoker_type newHighScoreEventInvoker;
   int high_score;
};
 
void print_new_high_score(int newHighScore)
{
   std::cout << "new high score = " << newHighScore << "\n";
}
 
int main()
{
   High_score hs;
 
   //can use a member function or lambda as well
   hs.newHighScoreEvent.attach(make_fun(&print_new_high_score));
 
   //submit a few scores
   hs.submit_score(1);
   hs.submit_score(3);
   hs.submit_score(2);
 
   hs.newHighScoreEvent.detach(make_fun(&print_new_high_score));
 
   //no effect because observer is detached
   hs.submit_score(4);
}

References

Smart Function Pointer Examples

Examples 1: function object equality

C++
auto lam = []() { /*...*/ };
auto f1 = make_fun(lam);
auto f2 = f1; //copy constructor
assert(f2 == f1); //always true after a copy constructor
f1(); //invoke operator() const
assert(f2 == f1); //since operator() is const equality was not changed
f2 = make_fun(lam); //create another lambda
assert(f1 == f2);
f1 = f2; //copy assignment
assert(f1 == f2); //always true after a copy assignment

Examples 2: member function object equality

C++
auto f1 = make_fun(&Some_class::some_mem_fun, &some_class);
auto f2 = d1; //copy constructor
assert(f1 == f2); //always true after a copy constructor
f1(); //invoke operator() const
assert(f2 == f1); //since operator() is const equality was not changed
f2 = make_fun(&Some_class::some_mem_fun, &some_class);
assert(f1 == f2); //target is now the same so equal
f1 = f2; //copy assignment
assert(f1 == f2); //always true alfter a copy assignment

fun_ptr Interface

Here is the interface for fun_ptr without implementation details.

C++
#ifndef FUN_PTR_INTERFACE_H
#define FUN_PTR_INTERFACE_H

template <typename FuncSignature>
class fun_ptr;

template <typename Ret, typename... Args>
class fun_ptr<Ret(Args...)>
{
public:
   fun_ptr() noexcept;

   fun_ptr(std::nullptr_t) noexcept;

   // not declared noexcept because of a narrow contract and optionally dynamic
   // memory allocation is allowed.
   fun_ptr(Ret (*func)(Args...));

   // not declared noexcept because of a narrow contract and optionally dynamic
   // memory allocation is allowed.
   template <typename T>
   fun_ptr(Ret (T::*method)(Args...), T* object);

   // not declared noexcept because of a narrow contract and optionally dynamic
   // memory allocation is allowed.
   template <typename T>
   fun_ptr(Ret (T::*method)(Args...) const, const T* object);

   // not declared noexcept because of a narrow contract and optionally dynamic
   // memory allocation is allowed.
   template <typename T, typename... PArgs>
   fun_ptr(Ret (T::*mem_fun_ptr)(PArgs...));

   // not declared noexcept because of a narrow contract and optionally dynamic
   // memory allocation is allowed.
   template <typename T, typename... PArgs>
   fun_ptr(Ret (T::*mem_fun_ptr)(PArgs...) const);

   // not declared noexcept because optionally dynamic memory allocation is
   // allowed.
   template <typename T>
   fun_ptr(T f);

   fun_ptr(const fun_ptr& rhs);

   fun_ptr(fun_ptr&& rhs) noexcept;

   ~fun_ptr() noexcept;

   fun_ptr& operator=(std::nullptr_t) noexcept;

   fun_ptr& operator=(const fun_ptr& rhs);

   fun_ptr& operator=(fun_ptr&& rhs) noexcept;

   explicit operator bool() const noexcept;

   template <typename... FwArgs>
   Ret operator()(FwArgs... args) const;
};

template <typename Ret, typename... Args>
inline bool operator==(const fun_ptr<Ret(Args...)>& lhs, const fun_ptr<Ret(Args...)>& rhs) noexcept;

template <typename Ret, typename... Args>
inline bool operator!=(const fun_ptr<Ret(Args...)>& lhs, const fun_ptr<Ret(Args...)>& rhs) noexcept;

template <typename Ret, typename... Args>
inline bool operator==(const fun_ptr<Ret(Args...)>& lhs, std::nullptr_t) noexcept;

template <typename Ret, typename... Args>
inline bool operator!=(const fun_ptr<Ret(Args...)>& lhs, std::nullptr_t) noexcept;

template <typename Ret, typename... Args>
inline bool operator==(std::nullptr_t, const fun_ptr<Ret(Args...)>& rhs) noexcept;

template <typename Ret, typename... Args>
inline bool operator!=(std::nullptr_t, const fun_ptr<Ret(Args...)>& rhs) noexcept;

template <typename Ret, typename... Args>
inline fun_ptr<Ret(Args...)> make_fun(Ret (*fp)(Args...));

template <typename Ret, typename T, typename... Args>
inline fun_ptr<Ret(Args...)> make_fun(Ret (T::*fp)(Args...), T* obj);

template <typename Ret, typename T, typename... Args>
inline fun_ptr<Ret(Args...)> make_fun(Ret (T::*fp)(Args...) const, const T* obj);

template <typename T>
inline auto make_fun(T functor) -> decltype(make_fun(&T::operator(), (T*) nullptr));

#endif // FUN_PTR_INTERFACE_H

event Interface

Here is the interface for event without implementation details.

C++
template <typename FuncSignature>
class event;

template <typename Ret, typename... Args>
class event<Ret(Args...)>
{
public:
   event(const event&) = delete;
   event(event&&) = delete;
   event& operator=(const event&) = delete;
   event& operator=(event&&) = delete;

   typedef util3::fun_ptr<Ret(Args...)> delegate_type;

   typedef delegate_type invoker_type;

   event();

   //! @brief retrieve the invoker for this event.
   //!
   //! This function can only be called once.
   invoker_type get_invoker();

   void attach(const delegate_type& theDelegate);

   void detach(const delegate_type& theDelegate);
};

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralExcellent ! Pin
cth02728-May-15 7:59
cth02728-May-15 7:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.