diff options
author | Jacques Lucke <jacques@blender.org> | 2021-02-23 13:47:01 +0300 |
---|---|---|
committer | Jacques Lucke <jacques@blender.org> | 2021-02-23 13:52:06 +0300 |
commit | aa4882506c9acb5483b506c5e1d1569ca8cf5dd3 (patch) | |
tree | bc5f8188b267e4a3caf173810d6e1778a7cc9dcd /source | |
parent | b2e1b13abde787c2aad97d5c317357cf84360bdb (diff) |
BLI: new FunctionRef type
Using `FunctionRef` is better than using `std::function`, templates and c function
pointers in some cases. The trade offs are explained in more detail in code documentation.
The following are some of the main benefits of using `FunctionRef`:
* It is convenient to use with all kinds of callables.
* It is cheaper to construct, copy and (possibly) call compared to `std::function`.
* Functions taking a `FunctionRef` as parameter don't need to be declared
in header files (as is necessary when using templates usually).
Differential Revision: https://developer.blender.org/D10476
Diffstat (limited to 'source')
-rw-r--r-- | source/blender/blenlib/BLI_function_ref.hh | 154 | ||||
-rw-r--r-- | source/blender/blenlib/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/blenlib/tests/BLI_function_ref_test.cc | 102 |
3 files changed, 258 insertions, 0 deletions
diff --git a/source/blender/blenlib/BLI_function_ref.hh b/source/blender/blenlib/BLI_function_ref.hh new file mode 100644 index 00000000000..86f761bbded --- /dev/null +++ b/source/blender/blenlib/BLI_function_ref.hh @@ -0,0 +1,154 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include <type_traits> +#include <utility> + +#include "BLI_utildefines.h" + +/** \file + * \ingroup bli + * + * A `FunctionRef<Signature>` is a non-owning reference to some callable object with a specific + * signature. It can be used to pass some callback to another function. + * + * A `FunctionRef` is small and cheap to copy. Therefore it should generally be passed by value. + * + * Example signatures: + * FunctionRef<void()> - A function without parameters and void return type. + * FunctionRef<int(float)> - A function with a float paramter and an int return value. + * FunctionRef<int(int, int)> - A function with two int parameters and an int return value. + * + * There are multiple ways to achieve that, so here is a comparison of the different approaches: + * 1. Pass function pointer and user data (as void *) separately: + * - The only method that is compatible with C interfaces. + * - Is cumbersome to work with in many cases, because one has to keep track of two parameters. + * - Not type safe at all, because of the void pointer. + * - It requires workarounds when one wants to pass a lambda into a function. + * 2. Using `std::function`: + * - It works well with most callables and is easy to use. + * - Owns the callable, so it can be returned from a function more safely than other methods. + * - Requires that the callable is copyable. + * - Requires an allocation when the callable is too large (typically > 16 bytes). + * 3. Using a template for the callable type: + * - Most efficient solution at runtime, because compiler knows the exact callable at the place + * where it is called. + * - Works well with all callables. + * - Requires the function to be in a header file. + * - It's difficult to constrain the signature of the function. + * 4. Using `FunctionRef`: + * - Second most efficient solution at runtime. + * - It's easy to constrain the signature of the callable. + * - Does not require the function to be in a header file. + * - Works well with all callables. + * - It's a non-owning reference, so it *cannot* be stored safely in general. + * + * The fact that this is a non-owning reference makes `FunctionRef` very well suited for some use + * cases, but one has to be a bit more careful when using it to make sure that the referenced + * callable is not destructed. + * + * In particular, one must not construct a `FunctionRef` variable from a lambda directly as shown + * below. This is because the lambda object goes out of scope after the line finished executing and + * will be destructed. Calling the reference afterwards invokes undefined behavior. + * + * Don't: + * FunctionRef<int()> ref = []() { return 0; }; + * Do: + * auto f = []() { return 0; }; + * FuntionRef<int()> ref = f; + * + * It is fine to pass a lambda directly to a function: + * + * void some_function(FunctionRef<int()> f); + * some_function([]() { return 0; }); + * + */ + +namespace blender { + +template<typename Function> class FunctionRef; + +template<typename Ret, typename... Params> class FunctionRef<Ret(Params...)> { + private: + /** + * A function pointer that knows how to call the referenced callable with the given parameters. + */ + Ret (*callback_)(intptr_t callable, Params... params) = nullptr; + + /** + * A pointer to the referenced callable object. This can be a C function, a lambda object or any + * other callable. + * + * The value does not need to be initialized because it is not used unless callback_ is set as + * well, in which case it will be initialized as well. + * + * Use `intptr_t` to avoid warnings when casting to function pointers. + */ + intptr_t callable_; + + template<typename Callable> static Ret callback_fn(intptr_t callable, Params... params) + { + return (*reinterpret_cast<Callable *>(callable))(std::forward<Params>(params)...); + } + + public: + FunctionRef() = default; + + /** + * A `FunctionRef` itself is a callable as well. However, we don't want that this + * constructor is called when `Callable` is a `FunctionRef`. If we would allow this, it + * would be easy to accidentally create a `FunctionRef` that internally calls another + * `FunctionRef`. Usually, when assigning a `FunctionRef` to another, we want that both + * contain a reference to the same underlying callable afterwards. + * + * It is still possible to reference another `FunctionRef` by first wrapping it in + * another lambda. + */ + template<typename Callable, + std::enable_if_t<!std::is_same_v<std::remove_cv_t<std::remove_reference_t<Callable>>, + FunctionRef>> * = nullptr> + FunctionRef(Callable &&callable) + : callback_(callback_fn<typename std::remove_reference_t<Callable>>), + callable_(reinterpret_cast<intptr_t>(&callable)) + { + } + + /** + * Call the referenced function and forward all parameters to it. + * + * This invokes undefined behavior if the `FunctionRef` does not reference a function currently. + */ + Ret operator()(Params... params) const + { + BLI_assert(callback_ != nullptr); + return callback_(callable_, std::forward<Params>(params)...); + } + + /** + * Returns true, when the `FunctionRef` references a function currently. + * If this returns false, the `FunctionRef` must not be called. + */ + operator bool() const + { + /* Just checking `callback_` is enough to determine if the `FunctionRef` is in a state that it + * can be called in. */ + return callback_ != nullptr; + } +}; + +} // namespace blender diff --git a/source/blender/blenlib/CMakeLists.txt b/source/blender/blenlib/CMakeLists.txt index 363d3003b3c..5a851b7b2cb 100644 --- a/source/blender/blenlib/CMakeLists.txt +++ b/source/blender/blenlib/CMakeLists.txt @@ -192,6 +192,7 @@ set(SRC BLI_float3.hh BLI_float4x4.hh BLI_fnmatch.h + BLI_function_ref.hh BLI_ghash.h BLI_gsqueue.h BLI_hash.h @@ -388,6 +389,7 @@ if(WITH_GTESTS) tests/BLI_disjoint_set_test.cc tests/BLI_edgehash_test.cc tests/BLI_expr_pylike_eval_test.cc + tests/BLI_function_ref_test.cc tests/BLI_ghash_test.cc tests/BLI_hash_mm2a_test.cc tests/BLI_heap_simple_test.cc diff --git a/source/blender/blenlib/tests/BLI_function_ref_test.cc b/source/blender/blenlib/tests/BLI_function_ref_test.cc new file mode 100644 index 00000000000..cdcbccc72e8 --- /dev/null +++ b/source/blender/blenlib/tests/BLI_function_ref_test.cc @@ -0,0 +1,102 @@ +/* Apache License, Version 2.0 */ + +#include "BLI_function_ref.hh" + +#include "testing/testing.h" + +namespace blender::tests { + +static int perform_binary_operation(int a, int b, FunctionRef<int(int, int)> operation) +{ + return operation(a, b); +} + +TEST(function_ref, StatelessLambda) +{ + const int result = perform_binary_operation(4, 6, [](int a, int b) { return a - b; }); + EXPECT_EQ(result, -2); +} + +TEST(function_ref, StatefullLambda) +{ + const int factor = 10; + const int result = perform_binary_operation( + 2, 3, [&](int a, int b) { return factor * (a + b); }); + EXPECT_EQ(result, 50); +} + +static int add_two_numbers(int a, int b) +{ + return a + b; +} + +TEST(function_ref, StandaloneFunction) +{ + const int result = perform_binary_operation(10, 5, add_two_numbers); + EXPECT_EQ(result, 15); +} + +TEST(function_ref, ConstantFunction) +{ + auto f = []() { return 42; }; + FunctionRef<int()> ref = f; + EXPECT_EQ(ref(), 42); +} + +TEST(function_ref, MutableStatefullLambda) +{ + int counter = 0; + auto f = [&]() mutable { return counter++; }; + FunctionRef<int()> ref = f; + EXPECT_EQ(ref(), 0); + EXPECT_EQ(ref(), 1); + EXPECT_EQ(ref(), 2); +} + +TEST(function_ref, Null) +{ + FunctionRef<int()> ref; + EXPECT_FALSE(ref); + + auto f = []() { return 1; }; + ref = f; + EXPECT_TRUE(ref); + + ref = {}; + EXPECT_FALSE(ref); +} + +TEST(function_ref, CopyDoesNotReferenceFunctionRef) +{ + auto f1 = []() { return 1; }; + auto f2 = []() { return 2; }; + FunctionRef<int()> x = f1; + FunctionRef<int()> y = x; + x = f2; + EXPECT_EQ(y(), 1); +} + +TEST(function_ref, CopyDoesNotReferenceFunctionRef2) +{ + auto f = []() { return 1; }; + FunctionRef<int()> x; + FunctionRef<int()> y = f; + FunctionRef<int()> z = static_cast<const FunctionRef<int()> &&>(y); + x = z; + y = {}; + EXPECT_EQ(x(), 1); +} + +TEST(function_ref, ReferenceAnotherFunctionRef) +{ + auto f1 = []() { return 1; }; + auto f2 = []() { return 2; }; + FunctionRef<int()> x = f1; + auto f3 = [&]() { return x(); }; + FunctionRef<int()> y = f3; + EXPECT_EQ(y(), 1); + x = f2; + EXPECT_EQ(y(), 2); +} + +} // namespace blender::tests |