[Bf-blender-cvs] [aa4882506c9] master: BLI: new FunctionRef type

Jacques Lucke noreply at git.blender.org
Tue Feb 23 11:53:09 CET 2021


Commit: aa4882506c9acb5483b506c5e1d1569ca8cf5dd3
Author: Jacques Lucke
Date:   Tue Feb 23 11:47:01 2021 +0100
Branches: master
https://developer.blender.org/rBaa4882506c9acb5483b506c5e1d1569ca8cf5dd3

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

===================================================================

A	source/blender/blenlib/BLI_function_ref.hh
M	source/blender/blenlib/CMakeLists.txt
A	source/blender/blenlib/tests/BLI_function_ref_test.cc

===================================================================

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



More information about the Bf-blender-cvs mailing list