☑️COMPLETE
This project is considered complete, and no further work will be done until requested otherwise.
Please open an issue on GitLab if you miss a feature or found a bug.
- Introduction and goals
- Library content
- Documentation
- Integration
- Tests
- Examples
- Comparison with other solutions
- Design decisions
- Motive for this library
- Feedback
- Thanks
- References
The C++ delegates library provides lightweight function wrappers able to store and invoke any callable target without the caller having to know the details of the callee. Examples of such callable targets are functions, member functions, function objects or lambda expressions. They just need to support the function call signature specified with the delegate.
This library is single header, written in C++14, and following the design goals:
- Safe to use:
- No undefined behavior, no matter how the delegates are used.
- Use compile-time checks instead of runtime-checks whenever possible.
- Efficiency:
- Ready to be used in memory and performance restricted software, e.g. on microcontrollers.
- Easy to understand, what the caller expects from the callee.
- Easy to use.
- Configurable behavior for calls when no target was assigned.
The library is tested to be compatible and free of compiler warnings with:
- C++14, C++23
- Clang: 4.0, 17.0, 18.1
- GCC: 5.3, 13.2, 14.1
- MSVC: 19.40
delegate<bool(int)> d_exp; // same as `delegate<bool(int), target_is_expected>`
d_exp(1); // throws exception, no target was assigned
d_exp = [](int i) { return i > 0; };
assert(d_exp(2) == true); // ok
delegate<bool(int), target_is_optional> d_opt1; // does not compile, non-void return
delegate<void(int), target_is_optional> d_opt2; // ok
d_opt2(3); // does nothing
delegate<bool(int), target_is_mandatory> d_mand1; // does not compile, no target assigned
delegate<bool(int), target_is_mandatory> d_mand2 = [](int i) { return i > 0; }; // ok
assert(d_mand2(4) == true); // ok
The basic delegate which supports any callable target of any function signature. On call, calls a previously assigned target. By default, throws an exception when called without assigned target. This behavior is configurable by the following options:
rome::target_is_expected
(default):
If no target was assigned before therome::delegate
is called, an exception is thrown.rome::target_is_optional
:
If no target was assigned before therome::delegate
is called, it directly returns without doing anything (only works if the function call signature hasvoid
return).rome::target_is_mandatory
Ensures by design that there is always a target assigned torome::delegate
. For this, the default constructor is deleted and there is no possibility to drop an assigned target.
See also the detailed documentation of rome::delegate
in doc/delegate.md.
fwd_delegate<bool()> d_nonvoid; // does not compile, non-void return
fwd_delegate<void(int&)> d_mut; // does not compile, mutable argument
fwd_delegate<void(const int&)> d_ok; // ok
Provides the same functionality as rome::delegate
, but with the restriction that data can only be forwarded. To ensure this, only function signatures with void
return and arguments of immutable type are allowed. E.g. the signature void(const std::string&)
would work, while void(int*)
or bool()
would produce a compile error.
See also the detailed documentation of rome::fwd_delegate
in doc/fwd_delegate.md.
event_delegate<void(int)> d;
d(1); // does nothing
d = [](int i) { std::cout << i; };
d(2); // prints "2"
A rome::fwd_delegate
that ignores calls if no target was assigned.
Designed for event or message-driven architectures, to notify about happened events. Thus, it is optional whether someone wants to listen to the event or not.
See also the detailed documentation of rome::event_delegate
in doc/fwd_delegate.md.
command_delegate<void(int)> d_err; // does not compile, no target assigned
command_delegate<void(int)> d_ok = [](int i) { std::cout << i; }; // ok
A rome::fwd_delegate
that ensures that always a target is assigned.
Designed for event or message-driven architectures to command an action that shall happen. Because the execution of the command is mandatory, a target must be assigned during construction of the delegate and can only be overridden by another target afterwards.
See also the detailed documentation of rome::command_delegate
in doc/fwd_delegate.md.
Please see the documentation in the folder ./doc
. Especially the following markdown files:
-
Add the folder
./include
to the include paths of your compiler or copy./include/rome/delegate.hpp
. -
Include the delegates:
#include <rome/delegate.hpp>
If exceptions are disabled, the delegates will call std::terminate()
instead (see also doc/delegate.md).
The delegates depend on the following headers of the C++ standard library:
<algorithm>
<cstddef>
<exception>
<new>
<type_traits>
<utility>
The tests can be found in ./test.
cmake --preset clang-cpp14-instr -B build
Setting ROME_DELEGATES_BUILD_TESTS=ON
configures the test targets. They are hidden otherwise.
Setting ROME_DELEGATES_INSTRUMENT=ON
enables instrumentation for code coverage, address sanitizer (ASan) and undefined behavior sanitizer (UBSan). Instrumentation only works with Clang.
Both options are enabled with the CMake presets clang-cpp14-instr
and clang-cpp23-instr
. See also the CMakePresets.json
.
cd build
-
ninja run_unittest
Test functionality and constraints of the delegates. The unit tests are built with-Wall -Wextra -pedantic -Werror
or/W4 /WX
for MSVC. Uses the unit test framework doctest and the mocking framework Trompeloeil.
IfROME_DELEGATES_INSTRUMENT
is enabled:- Prints errors of address sanitizer and undefined behavior sanitizer to stderr.
- Creates coverage data.
-
ninja coverage
:
Build and run the unit tests, collect coverage results, print the results to console, and create coverage reports inbuild/test/coverage
. -
ninja run_compile_error_tests
:
Test delegates for expected compile errors. -
ninja run_example_tests
:
Test that the provided examples are working. -
ninja clang_tidy
:
Run clang-tidy code analysis over the delegates and the unit tests.
Basic usage examples for all three types of Behavior
and the three target types function, member function and function object.
See the code in examples/basic_examples.cpp.
Or explore it online at Compiler Explorer.
#include <functional>
#include <iostream>
#include <rome/delegate.hpp>
void print(int i) {
std::cout << i << '\n';
}
int plus100(int i) {
return i + 100;
}
struct TargetClass {
int value = 0;
void set(int i) {
value = i;
}
};
struct Example {
rome::delegate<int(int), rome::target_is_mandatory> onMandatoryNotEmpty;
rome::delegate<int(int) /*, rome::target_is_expected*/> onExpectedNotEmpty; // (1)
// rome::delegate<int(int), rome::target_is_optional> onMaybeEmpty; // (2) does not compile
rome::delegate<void(int), rome::target_is_optional> onMaybeEmpty;
Example(decltype(onMandatoryNotEmpty)&& mand) : onMandatoryNotEmpty{std::move(mand)} { // (3)
}
};
int main() {
TargetClass obj{};
Example x{std::negate<>{}}; // (3)
std::cout << "Calls after initialization:\n";
print(x.onMandatoryNotEmpty(1)); // `std::negate<>` was assigned
try {
x.onExpectedNotEmpty(2); // called empty
}
catch (const rome::bad_delegate_call& e) {
std::cout << e.what() << '\n';
}
x.onMaybeEmpty(3); // called empty
std::cout << "\nCalls with fresh assigned targets:\n";
// assign function object
x.onMandatoryNotEmpty = [](int i) { return i + 10; };
// assign function object wrapping a function
x.onExpectedNotEmpty = [](int i) { return plus100(i); };
// assign function object wrapping a member function
x.onMaybeEmpty = [&obj](int i) { obj.set(i); };
print(x.onMandatoryNotEmpty(4));
print(x.onExpectedNotEmpty(5));
print(obj.value);
x.onMaybeEmpty(6);
print(obj.value);
std::cout << "\nCalls after dropping targets:\n";
// x.onMandatoryNotEmpty = nullptr; // (4) does not compile
x.onExpectedNotEmpty = nullptr;
x.onMaybeEmpty = nullptr;
print(x.onMandatoryNotEmpty(7)); // function object still assigned
try {
x.onExpectedNotEmpty(8); // called empty
}
catch (const rome::bad_delegate_call& e) {
std::cout << e.what() << '\n';
}
x.onMaybeEmpty(9); // called empty
print(obj.value);
}
- (1) - second template parameter is
rome::target_is_expected
by default - (2) -
rome::delegate
withrome::target_is_optional
must have void return - (3) -
rome::delegate
withrome::target_is_mandatory
has deleted default constructor, a target must be assigned during construction - (4) -
rome::delegate
withrome::target_is_mandatory
does not allow to drop targets
Output:
Calls after initialization:
-1
rome::bad_delegate_callCalls with fresh assigned targets:
14
105
0
6Calls after dropping targets:
17
rome::bad_delegate_call
6
Model of an extremely simplified cruise control system. The four classes Engine, BrakingSystem, SpeedSensor and CruiseControl are atomic, i.e., are free from dependencies to other classes. Integration integrates all four.
See the code in examples/cruise_control.cpp.
Or explore it online at Compiler Explorer.
#include <iostream>
#include <utility>
#include <rome/delegate.hpp>
struct Engine {
void accelerate() {
std::cout << "engine accelerating\n";
}
void turnOff() {
std::cout << "engine turned off\n";
}
};
struct BrakingSystem {
void turnBrakesOn() {
std::cout << "brakes on\n";
}
void turnBrakesOff() {
std::cout << "brakes off\n";
}
};
struct SpeedSensor {
// Assigning delegate is optional for speed sensor to work.
rome::event_delegate<void(float)> onSpeedChanged;
};
class CruiseControl {
float targetSpeed_ = 0.0F;
// Assigning both delegates is required for cruise control to work.
rome::command_delegate<void()> onAccelerateCar_;
rome::command_delegate<void()> onSlowDownCar_;
public:
void updateAcceleration(const float drivingSpeed) {
if (drivingSpeed < targetSpeed_ * 0.95F) {
onAccelerateCar_();
}
else if (drivingSpeed > targetSpeed_ * 1.05F) {
onSlowDownCar_();
}
}
void setTargetSpeed(const float targetSpeed) {
targetSpeed_ = targetSpeed;
}
CruiseControl(rome::command_delegate<void()>&& onAccelerateCar,
rome::command_delegate<void()>&& onSlowDownCar)
: onAccelerateCar_{std::move(onAccelerateCar)}, onSlowDownCar_{std::move(onSlowDownCar)} {
}
};
struct Integration {
SpeedSensor speedSensor;
CruiseControl cruiseControl;
Engine engine;
BrakingSystem brakes;
Integration()
: cruiseControl{
[this]() {
brakes.turnBrakesOff();
engine.accelerate();
},
[this]() {
engine.turnOff();
brakes.turnBrakesOn();
}} {
speedSensor.onSpeedChanged = [this](float drivingSpeed) {
cruiseControl.updateAcceleration(drivingSpeed);
};
}
};
Integration integration{};
int main() {
// Simulate IO not connected in this example
integration.cruiseControl.setTargetSpeed(25.0F);
integration.speedSensor.onSpeedChanged(20.0F);
integration.speedSensor.onSpeedChanged(25.0F);
integration.speedSensor.onSpeedChanged(30.0F);
}
Output:
brakes off
engine accelerating
engine turned off
brakes on
Similar C++ standard library counterparts in behavior and interface are:
std::move_only_function
(C++23)
Very similar by constraints and efficiency. However, it is undefined behavior when astd::move_only_function
is called empty. For C++ delegates, this behavior is always defined and configurable.std::function
(C++11)
Copyable but less efficient. Throws an exception when called empty.
-
Why do delegates take ownership of assigned function objects?
Let's take the exampledelegate<void()> d = []() {};
. If the delegate would only reference the function object created by the lambda expression, it may easily happen, that the assigned function object is already destroyed when the delegate is called. To make safe usage of the delegate easy and unsafe usage hard, the delegate takes the ownership of the function object.
Note that it is still necessary to manage the lifetime of objects that the assigned function object captures by reference. -
Why is the size of the delegate
sizeof(void*) + 2*sizof(void(*)())
?- One function pointer stores the function that invokes the target.
- One function pointer stores the function that destroys an assigned function object. This happens either when the delegate is destroyed or when a target is dropped.
- The object pointer stores the address where the function object is stored. Or, with small object optimization, the function object is stored within the memory of the object pointer.
Thus, the size is kept at the required minimum, with memory restricted devices in mind.
-
Why can't I copy delegates, why are they move-only?
- It would need another function pointer stored within the delegate that increases its size significantly.
- Copying the delegate may only lead to a shallow copy of the target, but a deep copy might be needed. Because the delegate hides the assigned target, this issue is invisible.
As a result, the C++ delegates are move only. If you need multiple instances of the same delegate, just create multiple delegates and assign the same target.
-
Why can't I directly assign a function to a C++ delegate, as for example with
std::move_only_function
?
Because it is less optimizable and may lead to less efficient code.- With direct assignment
delegate<void()> d = &function;
:
The address of the function or member function is passed to the assignment operator's function argument. Thus, it is only known during runtime. - When wrapped by a function object
delegate<void()> d = []() { function(); };
:
The function is now bound to the type of the function object. Therefore, it is known during compilation time.
- With direct assignment
-
Why is the size for small object optimization
sizeof(void*)
?
When a big function object is assigned to a C++ delegate, the delegate needs to store the object in a dynamic allocated memory and remember that location with a pointer (sizeof(void*)
). If, however, the function object is smaller or equal to the pointer, the space of the pointer can instead be used to store the function object in. This enables to small buffer optimize any lambda expression that only captures a reference or a pointer. Additional data may be accessed through that reference/pointer.
As a result, dynamic allocation should be avoidable in any use case without increasing the size of the delegate. -
Why is the namespace called rome?
It has nothing to do with the Italian capital, it's just the initials of my name.
The initial motivation was the wiring between independent modules.
Let A
and B
be two modules. A
is reactive and may produce the event done
. When A
produces the event done
, B
shall start
.
namespace module_a {
struct A {
/* something that produces the event `done` */
};
}
namespace module_b {
struct B {
void start();
};
}
void wire() {
// connect them somehow
}
In OOP, this is commonly done by A
having an association to an interface I
and B
realizing that interface. B
is then injected into A
by its base class I
.
It might look something like the following:
namespace awkward {
struct I {
virtual void done() = 0;
};
}
namespace module_a {
struct A {
awkward::I& done;
};
}
namespace module_b {
struct B : public awkward::I {
void done() override; // but should be called `start`
};
}
void wire() {
module_b::B b{};
module_a::A a{b};
}
The consequences are:
- The code of
I
needs to be stored somewhere, either in moduleA
, moduleB
, or a third module.- If
I
is stored inA
orB
, one module depends on the other. They are not independent anymore. - If
I
is stored in a third module, that third module is created just for that purpose, whileA
andB
still are not as independent anymore as without.
- If
- By realizing
I
inB
,I
becomes a public interface of some class inB
. However, the function names ofI
might be a bad fit for the public interface ofB
, so you need to add an extra class and hide it somehow, just to realizeI
.
A better solution is to use std::function
for the event done
:
namespace module_a {
struct A {
std::function<void()> done;
};
}
namespace b {
struct B {
void start();
};
}
void wire() {
module_a::A a{};
module_b::B b{};
a.done = [&b]() { b.start(); };
}
While already a lot better than the OOP interface headache, std::function
isn't exactly lightweight. On memory restricted devices this might become a problem. Furthermore, std::function
throws an exception if no target was assigned. This enforces to assign a target to any provided event, whether handling that event is of interest or not. And if the handling of an event is required, a compile error is preferable to a runtime exception.
So, a lightweight solution similar to std::function
is needed, that enables marking the handling of an event as optional or mandatory, with unhandled optional events doing nothing and unhandled mandatory events raising a compile error.
That's where my journey started and the reason for the rome::event_delegate
and the rome::command_delegate
.
Do you have any request, a question or found a bug? Please open an issue on GitLab or write me an email. I am also happy to hear from any positive or negative experience you had with this library.
- Sergey Ryazanov for his incredible article The Impossibly Fast C++ Delegates. He explains how you can use the fact that the address of any function can be passed as non-type template argument to create highly optimizable function delegates.
- Lee David for help finding more comprehensible names.
- Matthias Deimbacher for the initial set up of the CI pipelines at GitLab.
- Ryazanov, Sergey. “The Impossibly Fast C++ Delegates.” The Impossibly Fast C++ Delegates, CodeProject, 18 July 2005, https://www.codeproject.com/Articles/11015/The-Impossibly-Fast-C-Delegates, MIT.
- Cppreference. std::move_only_function, cppreference.com, 14 November 2023, https://en.cppreference.com/w/cpp/utility/functional/move_only_function, CC-BY-SA.
- Cppreference. std::function, cppreference.com, 14 November 2023, https://en.cppreference.com/w/cpp/utility/functional/function, CC-BY-SA.
- Kirilov, Viktor. doctest, 15 March 2023, https://github.com/onqtam/doctest, MIT.
- Fahler, Björn. Trompeloeil, 21 July 2023, https://github.com/rollbear/trompeloeil, BSL-1.0.