Skip to content

Commit

Permalink
feat: Add Delegate type (#1059)
Browse files Browse the repository at this point in the history
This type is intended as a sort-of replacement of `std::function` without the virtual function overhead of that type. This is ultimately meant to support the new extension mechanism for the KF and CKF, where these delegates will be used for the calibrator, smoother, and so on.

Delegate type that allows type erasure of a callable without allocation and with a single level of indirection. This type can support:
- a free function pointer
-  - a pointer to a member function alongside an instance pointer
Note: `Delegate` does not assume ownership of the instance. You need to ensure that the lifetime of the callable   instance is longer than that of the @c Delegate.
Currently `Delegate` only supports callables that are `const`.

The usage of this type looks like this for a free function:

```cpp
int sumImplementation(int a, int b) {
  return a + b;
}

Delegate<int(int, int)> sum;
sum.connect<&sumImplementation>();

int c = sum(2, 2); // = 4
```

and like this for a member pointer with instance:

```cpp
struct Subtractor {
  int x;

  int subtract(int v) const {
    return v - x;
  }
};

Delegate<int(int)> subtract;

Subtractor instance{5};

subtract.connect<&Subtractor::subtract>(&instance);

int y = subtract(10); // = 5
```
  • Loading branch information
paulgessinger committed Nov 9, 2021
1 parent 2e48e5c commit 2246d1a
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 0 deletions.
120 changes: 120 additions & 0 deletions Core/include/Acts/Utilities/Delegate.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// This file is part of the Acts project.
//
// Copyright (C) 2021 CERN for the benefit of the Acts project
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#pragma once

#include <cassert>
#include <functional>

namespace Acts {

template <typename>
class Delegate;

/// Delegate type that allows type erasure of a callable without allocation and
/// with a single level of indirection.
/// This type can support:
/// - a free function pointer
/// - a pointer to a member function alongside an instance pointer
/// @note @c Delegate does not assume ownership of the instance.
/// You need to ensure that the lifetime of the callable
/// instance is longer than that of the @c Delegate.
/// @note Currently @c Delegate only supports callables that are ``const``
/// @tparam R Return type of the function signature
/// @tparam Args Types of the arguments of the function signatures
///
template <typename R, typename... Args>
class Delegate<R(Args...)> {
/// Alias of the return type
using return_type = R;
/// Alias to the function pointer type this class will store
using function_type = return_type (*)(const void*, Args...);

public:
Delegate() = default;

/// Constructor with an explicit runtime callable
/// @param callable The runtime value of the callable
/// @note The function signature requires the first argument of the callable is `const void*`.
/// i.e. if the signature of the delegate is `void(int)`, the callable's
/// signature has to be `void(const void*, int)`.
Delegate(function_type callable) { connect(callable); }

/// Assignment operator with an explicit runtime callable
/// @param callable The runtime value of the callable
/// @note The function signature requires the first argument of the callable is `const void*`.
/// i.e. if the signature of the delegate is `void(int)`, the callable's
/// signature has to be `void(const void*, int)`.
void operator=(function_type callable) { connect(callable); }

/// Connect a free function pointer.
/// @note The function pointer must be ``constexpr`` for @c Delegate to accept it
/// @tparam Callable The compile-time free function pointer
template <auto Callable>
void connect() {
m_payload = nullptr;
m_function = [](const void* /*payload*/, Args... args) -> return_type {
return std::invoke(Callable, std::forward<Args>(args)...);
};
}

/// Connect anything that is assignable to the function pointer
/// @param callable The runtime value of the callable
/// @note The function signature requires the first argument of the callable is `const void*`.
/// i.e. if the signature of the delegate is `void(int)`, the callable's
/// signature has to be `void(const void*, int)`.
void connect(function_type callable) {
m_payload = nullptr;
m_function = callable;
}

/// Connect a member function to be called on an instance
/// @tparam Callable The compile-time member function pointer
/// @tparam Type The type of the instance the member function should be called on
/// @param instance The instance on which the member function pointer should be called on
/// @note @c Delegate does not assume owner ship over @p instance. You need to ensure
/// it's lifetime is longer than that of @c Delegate.
template <auto Callable, typename Type>
void connect(const Type* instance) {
m_payload = instance;
m_function = [](const void* payload, Args... args) -> return_type {
const auto* concretePayload = static_cast<const Type*>(payload);
return std::invoke(Callable, concretePayload,
std::forward<Args>(args)...);
};
}

/// The call operator that exposes the functionality of the @c Delegate type.
/// @param args The arguments to call the contained function with
/// @return Return value of the contained function
return_type operator()(Args... args) const {
assert(connected() && "Delegate is not connected");
return std::invoke(m_function, m_payload, std::forward<Args>(args)...);
}

/// Return whether this delegate is currently connected
/// @return True if this delegate is connected
bool connected() const { return m_function != nullptr; }

/// Return whether this delegate is currently connected
/// @return True if this delegate is connected
operator bool() const { return connected(); }

/// Disconnect this delegate, meaning it cannot be called anymore
void disconnect() {
m_payload = nullptr;
m_function = nullptr;
}

private:
/// Stores the instance pointer
const void* m_payload{nullptr};
/// Stores the function pointer wrapping the compile time function pointer given in @c connect().
function_type m_function{nullptr};
};
} // namespace Acts
1 change: 1 addition & 0 deletions Tests/UnitTests/Core/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ add_unittest(Result ResultTests.cpp)
add_unittest(Subspace SubspaceTests.cpp)
add_unittest(TypeTraits TypeTraitsTest.cpp)
add_unittest(UnitVectors UnitVectorsTests.cpp)
add_unittest(Delegate DelegateTests.cpp)
if (ACTS_BUILD_CUDA_FEATURES)
add_unittest(Cuda CudaTests.cu)
add_unittest(CudaMostSimplified CudaMostSimplifiedTests.cu)
Expand Down
127 changes: 127 additions & 0 deletions Tests/UnitTests/Core/Utilities/DelegateTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// This file is part of the Acts project.
//
// Copyright (C) 2021 CERN for the benefit of the Acts project
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <boost/test/data/test_case.hpp>
#include <boost/test/unit_test.hpp>

#include "Acts/Tests/CommonHelpers/FloatComparisons.hpp"
#include "Acts/Utilities/Delegate.hpp"

#include <numeric>
#include <optional>
#include <random>
#include <tuple>

using namespace Acts;

namespace bd = boost::unit_test::data;

BOOST_AUTO_TEST_SUITE(DelegateTests)

int sumImpl(int a, int b) {
return a + b;
}

BOOST_AUTO_TEST_CASE(ConnectConstexprLambda) {
Delegate<int(int, int)> sum;
BOOST_CHECK(!sum);
BOOST_CHECK(!sum.connected());

sum.connect<&sumImpl>();

BOOST_CHECK_EQUAL(sum(2, 5), 7);
BOOST_CHECK_NE(sum(2, 3), 7);

sum.connect([](const void*, int a, int b) -> int { return a + b; });

BOOST_CHECK(sum);
BOOST_CHECK(sum.connected());

BOOST_CHECK_EQUAL(sum(2, 5), 7);
BOOST_CHECK_NE(sum(2, 3), 7);
}

float multiply(float a, float b) {
return a * b;
}

BOOST_AUTO_TEST_CASE(ConnectFunctionPointer) {
Delegate<float(float, float)> mult;

BOOST_CHECK(!mult);
BOOST_CHECK(!mult.connected());

mult.connect<multiply>();

BOOST_CHECK(mult);
BOOST_CHECK(mult.connected());

CHECK_CLOSE_REL(mult(2, 5.9), 2 * 5.9, 1e-6);
BOOST_CHECK_NE(mult(2, 3.2), 58.9);
}

struct Subtractor {
int v;
int execute(int a) const { return a - v; }
};

BOOST_AUTO_TEST_CASE(ConnectStruct) {
Delegate<int(int)> sub;

BOOST_CHECK(!sub);
BOOST_CHECK(!sub.connected());

Subtractor s{18};
sub.connect<&Subtractor::execute>(&s);

BOOST_CHECK(sub);
BOOST_CHECK(sub.connected());

BOOST_CHECK_EQUAL(sub(7), 7 - 18);
}

int addition(const void*, int a, int b) {
return a + b;
}

BOOST_AUTO_TEST_CASE(ConnectRuntime) {
{
Delegate<int(int, int)> add;
BOOST_CHECK(!add);
BOOST_CHECK(!add.connected());

add.connect(&addition);
BOOST_CHECK(add);
BOOST_CHECK(add.connected());

BOOST_CHECK_EQUAL(add(4, 4), 8);
}

{
Delegate<int(int, int)> add{&addition};

BOOST_CHECK(add);
BOOST_CHECK(add.connected());

BOOST_CHECK_EQUAL(add(4, 4), 8);
}

{
Delegate<int(int, int)> add;
BOOST_CHECK(!add);
BOOST_CHECK(!add.connected());

add = &addition;
BOOST_CHECK(add);
BOOST_CHECK(add.connected());

BOOST_CHECK_EQUAL(add(4, 4), 8);
}
}

BOOST_AUTO_TEST_SUITE_END()

0 comments on commit 2246d1a

Please sign in to comment.