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:
- be substitutable with a copy
- 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.
- 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:
- If the targets are different types return false
- Else if both targets are nullptr return true
- Else if both targets are standalone functions
- Return the comparison of the function pointers
- Else if both targets are member functions (without instance pointers)
- If member functions point to different classes return false
- Else return the comparison of the member function pointers
- Else if both targets are member function pointers /w instance pointers
- If targets are different classes return false
- Else if instance pointers are different return false
- Else return comparison of the function pointers (unspecified if at least one is virtual)
- Else if both targets are function objects with operator==
- Return result of operator==
- Else if both targets are function objects without operator==
- If function object types are different return false
- 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.
class High_score
{
public:
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);
}
}
private:
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;
hs.newHighScoreEvent.attach(make_fun(&print_new_high_score));
hs.submit_score(1);
hs.submit_score(3);
hs.submit_score(2);
hs.newHighScoreEvent.detach(make_fun(&print_new_high_score));
hs.submit_score(4);
}
References
Smart Function Pointer Examples
Examples 1: function object equality
auto lam = []() { };
auto f1 = make_fun(lam);
auto f2 = f1; assert(f2 == f1); f1(); assert(f2 == f1); f2 = make_fun(lam); assert(f1 == f2);
f1 = f2; assert(f1 == f2);
Examples 2: member function object equality
auto f1 = make_fun(&Some_class::some_mem_fun, &some_class);
auto f2 = d1; assert(f1 == f2); f1(); assert(f2 == f1); f2 = make_fun(&Some_class::some_mem_fun, &some_class);
assert(f1 == f2); f1 = f2; assert(f1 == f2);
fun_ptr Interface
Here is the interface for fun_ptr
without implementation details.
#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;
fun_ptr(Ret (*func)(Args...));
template <typename T>
fun_ptr(Ret (T::*method)(Args...), T* object);
template <typename T>
fun_ptr(Ret (T::*method)(Args...) const, const T* object);
template <typename T, typename... PArgs>
fun_ptr(Ret (T::*mem_fun_ptr)(PArgs...));
template <typename T, typename... PArgs>
fun_ptr(Ret (T::*mem_fun_ptr)(PArgs...) const);
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.
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();
invoker_type get_invoker();
void attach(const delegate_type& theDelegate);
void detach(const delegate_type& theDelegate);
};