[llvm] [orc-rt] make bind_front match std::bind_front semantics. (PR #226675)

Lang Hames via llvm-commits llvm-commits at lists.llvm.org
Sat Sep 26 03:37:24 PDT 2026


https://github.com/lhames created https://github.com/llvm/llvm-project/pull/226675

orc_rt::bind_front stands in for std::bind_front until the ORC runtime can use C++20, so code written against it should keep working after the switch. This commit brings its behavior in line with std::bind_front:

- noexcept propagation
- const and rvalue call operators, forwarding the wrapper's qualifiers
- no fallback to a differently-qualified call operator
- reference return types
- member pointers
- copyable wrappers, including nested binds

The unit tests are reorganized and extended to cover these.

Assisted-by: Claude

>From 838ec9f05e7a05f30a88d6dde5a4057b67e9d56f Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Sat, 26 Sep 2026 13:27:02 +1000
Subject: [PATCH] [orc-rt] make bind_front match std::bind_front semantics.

orc_rt::bind_front stands in for std::bind_front until the ORC runtime can
use C++20, so code written against it should keep working after the switch.
This commit brings its behavior in line with std::bind_front:

- noexcept propagation
- const and rvalue call operators, forwarding the wrapper's qualifiers
- no fallback to a differently-qualified call operator
- reference return types
- member pointers
- copyable wrappers, including nested binds

The unit tests are reorganized and extended to cover these.

Assisted-by: Claude
---
 orc-rt/include/orc-rt/support/bind.h   |  88 +++++++-
 orc-rt/test/unit/support/bind-test.cpp | 278 ++++++++++++++++++++++---
 2 files changed, 333 insertions(+), 33 deletions(-)

diff --git a/orc-rt/include/orc-rt/support/bind.h b/orc-rt/include/orc-rt/support/bind.h
index 1cfa265c3f0cc..986baa19d8d3c 100644
--- a/orc-rt/include/orc-rt/support/bind.h
+++ b/orc-rt/include/orc-rt/support/bind.h
@@ -15,30 +15,96 @@
 #ifndef ORC_RT_SUPPORT_BIND_H
 #define ORC_RT_SUPPORT_BIND_H
 
+#include <functional>
 #include <tuple>
 #include <type_traits>
+#include <utility>
 
 namespace orc_rt {
 namespace detail {
 
+/// Call wrapper returned by bind_front. Matches the behavior of the C++20
+/// std::bind_front perfect forwarding call wrapper: the wrapper's cv and
+/// value category are forwarded to the stored callable and bound arguments.
 template <typename Fn, typename... BoundArgTs> class BoundFn {
 private:
-  template <size_t... Is, typename... ArgTs>
-  auto callExpandingBound(std::index_sequence<Is...>, ArgTs &&...Args) {
-    return F(std::get<Is>(BoundArgs)..., std::forward<ArgTs>(Args)...);
+  template <typename Self, size_t... Is, typename... ArgTs>
+  static decltype(auto) callExpandingBound(Self &&S, std::index_sequence<Is...>,
+                                           ArgTs &&...Args) {
+    return std::invoke(std::forward<Self>(S).F,
+                       std::get<Is>(std::forward<Self>(S).BoundArgs)...,
+                       std::forward<ArgTs>(Args)...);
   }
 
 public:
-  template <typename FnInit, typename... BoundArgInitTs>
-  BoundFn(FnInit &&F, BoundArgInitTs &&...BoundArgs)
+  template <typename FnInit, typename... BoundArgInitTs,
+            typename = std::enable_if_t<
+                !std::is_same_v<std::decay_t<FnInit>, BoundFn>>>
+  explicit BoundFn(FnInit &&F, BoundArgInitTs &&...BoundArgs) noexcept(
+      std::is_nothrow_constructible_v<Fn, FnInit &&> &&
+      (std::is_nothrow_constructible_v<BoundArgTs, BoundArgInitTs &&> && ...))
       : F(std::forward<FnInit>(F)),
         BoundArgs(std::forward<BoundArgInitTs>(BoundArgs)...) {}
 
-  template <typename... ArgTs> auto operator()(ArgTs &&...Args) {
-    return callExpandingBound(std::index_sequence_for<BoundArgTs...>(),
+  // Each call operator below is paired with a deleted overload that is
+  // selected only when the call is ill-formed. This prevents fallback to a
+  // differently-qualified overload (e.g. a non-const call silently using the
+  // const overload), matching std::bind_front.
+
+  template <typename... ArgTs>
+  std::invoke_result_t<Fn &, BoundArgTs &..., ArgTs...>
+  operator()(ArgTs &&...Args) & noexcept(
+      std::is_nothrow_invocable_v<Fn &, BoundArgTs &..., ArgTs...>) {
+    return callExpandingBound(*this, std::index_sequence_for<BoundArgTs...>(),
+                              std::forward<ArgTs>(Args)...);
+  }
+
+  template <typename... ArgTs, typename = std::enable_if_t<!std::is_invocable_v<
+                                   Fn &, BoundArgTs &..., ArgTs...>>>
+  void operator()(ArgTs &&...) & = delete;
+
+  template <typename... ArgTs>
+  std::invoke_result_t<const Fn &, const BoundArgTs &..., ArgTs...>
+  operator()(ArgTs &&...Args) const & noexcept(
+      std::is_nothrow_invocable_v<const Fn &, const BoundArgTs &...,
+                                  ArgTs...>) {
+    return callExpandingBound(*this, std::index_sequence_for<BoundArgTs...>(),
+                              std::forward<ArgTs>(Args)...);
+  }
+
+  template <typename... ArgTs,
+            typename = std::enable_if_t<!std::is_invocable_v<
+                const Fn &, const BoundArgTs &..., ArgTs...>>>
+  void operator()(ArgTs &&...) const & = delete;
+
+  template <typename... ArgTs>
+  std::invoke_result_t<Fn &&, BoundArgTs &&..., ArgTs...>
+  operator()(ArgTs &&...Args) && noexcept(
+      std::is_nothrow_invocable_v<Fn &&, BoundArgTs &&..., ArgTs...>) {
+    return callExpandingBound(std::move(*this),
+                              std::index_sequence_for<BoundArgTs...>(),
                               std::forward<ArgTs>(Args)...);
   }
 
+  template <typename... ArgTs, typename = std::enable_if_t<!std::is_invocable_v<
+                                   Fn &&, BoundArgTs &&..., ArgTs...>>>
+  void operator()(ArgTs &&...) && = delete;
+
+  template <typename... ArgTs>
+  std::invoke_result_t<const Fn &&, const BoundArgTs &&..., ArgTs...>
+  operator()(ArgTs &&...Args) const && noexcept(
+      std::is_nothrow_invocable_v<const Fn &&, const BoundArgTs &&...,
+                                  ArgTs...>) {
+    return callExpandingBound(std::move(*this),
+                              std::index_sequence_for<BoundArgTs...>(),
+                              std::forward<ArgTs>(Args)...);
+  }
+
+  template <typename... ArgTs,
+            typename = std::enable_if_t<!std::is_invocable_v<
+                const Fn &&, const BoundArgTs &&..., ArgTs...>>>
+  void operator()(ArgTs &&...) const && = delete;
+
 private:
   Fn F;
   std::tuple<BoundArgTs...> BoundArgs;
@@ -49,6 +115,14 @@ template <typename Fn, typename... BoundArgTs> class BoundFn {
 template <typename Fn, typename... BoundArgTs>
 detail::BoundFn<std::decay_t<Fn>, std::decay_t<BoundArgTs>...>
 bind_front(Fn &&F, BoundArgTs &&...BoundArgs) {
+  static_assert(std::is_constructible_v<std::decay_t<Fn>, Fn> &&
+                    std::is_move_constructible_v<std::decay_t<Fn>>,
+                "bind_front requires a move-constructible callable");
+  static_assert(
+      ((std::is_constructible_v<std::decay_t<BoundArgTs>, BoundArgTs> &&
+        std::is_move_constructible_v<std::decay_t<BoundArgTs>>) &&
+       ...),
+      "bind_front requires move-constructible bound arguments");
   return detail::BoundFn<std::decay_t<Fn>, std::decay_t<BoundArgTs>...>(
       std::forward<Fn>(F), std::forward<BoundArgTs>(BoundArgs)...);
 }
diff --git a/orc-rt/test/unit/support/bind-test.cpp b/orc-rt/test/unit/support/bind-test.cpp
index c852256635d1f..80c3b51386c73 100644
--- a/orc-rt/test/unit/support/bind-test.cpp
+++ b/orc-rt/test/unit/support/bind-test.cpp
@@ -6,30 +6,79 @@
 //
 //===----------------------------------------------------------------------===//
 //
-// Tests for orc-rt's bind-test.h APIs.
+// Tests for orc-rt's bind.h APIs.
 //
 //===----------------------------------------------------------------------===//
 
 #include "CommonTestUtils.h"
+#include "orc-rt-c/config.h"
 #include "orc-rt/support/bind.h"
 #include "orc-rt/support/move_only_function.h"
 #include "gtest/gtest.h"
 
+#include <memory>
+#include <type_traits>
+#include <utility>
+
 using namespace orc_rt;
 using namespace orc_rt::test;
 
-static void voidVoid(void) {}
+static int addInts(int X, int Y) { return X + Y; }
+static int subtract(int X, int Y) { return X - Y; }
+static int increment(int N) { return N + 1; }
+static void doNothing() {}
+static int noexceptAdd(int X, int Y) noexcept { return X + Y; }
 
-TEST(BindTest, VoidVoid) {
-  auto B = bind_front(voidVoid);
-  B();
+namespace {
+
+/// The cv qualification and value category of a call's object or argument.
+enum class Category { LValue, ConstLValue, RValue, ConstRValue };
+
+} // namespace
+
+template <typename T> static constexpr Category categoryOf() {
+  constexpr bool IsConst = std::is_const_v<std::remove_reference_t<T>>;
+  if constexpr (std::is_lvalue_reference_v<T>)
+    return IsConst ? Category::ConstLValue : Category::LValue;
+  else
+    return IsConst ? Category::ConstRValue : Category::RValue;
 }
 
-static int addInts(int X, int Y) { return X + Y; }
+namespace {
 
-TEST(BindTest, SimpleBind) {
-  auto Add1 = bind_front(addInts, 1);
-  EXPECT_EQ(Add1(2), 3);
+/// Reports which of its call operators was selected.
+struct CategoryOfCall {
+  Category operator()() & { return Category::LValue; }
+  Category operator()() const & { return Category::ConstLValue; }
+  Category operator()() && { return Category::RValue; }
+  Category operator()() const && { return Category::ConstRValue; }
+};
+
+/// Reports how its argument was passed.
+struct CategoryOfArg {
+  template <typename T> Category operator()(T &&) const {
+    return categoryOf<T &&>();
+  }
+};
+
+struct Counter {
+  int Value = 0;
+  int add(int N) { return Value += N; }
+};
+
+/// Copyable, but copying is not noexcept.
+struct MayThrowOnCopy {
+  MayThrowOnCopy() = default;
+  MayThrowOnCopy(const MayThrowOnCopy &) {}
+};
+
+} // namespace
+
+// Basic binding.
+
+TEST(BindTest, BindsLeadingArguments) {
+  auto SubtractFrom10 = bind_front(subtract, 10);
+  EXPECT_EQ(SubtractFrom10(3), 7);
 }
 
 TEST(BindTest, NoBoundArguments) {
@@ -42,15 +91,52 @@ TEST(BindTest, NoFreeArguments) {
   EXPECT_EQ(Add1And2(), 3);
 }
 
-TEST(BindTest, LambdaCapture) {
+TEST(BindTest, VoidReturn) {
+  auto B = bind_front(doNothing);
+  B();
+  EXPECT_TRUE((std::is_void_v<decltype(B())>));
+}
+
+// Kinds of callable.
+
+TEST(BindTest, BindsLambda) {
   auto Add1 = bind_front([](int X, int Y) { return X + Y; }, 1);
   EXPECT_EQ(Add1(2), 3);
 }
 
-TEST(BindTest, MinimalMoves) {
+TEST(BindTest, BindsMoveOnlyCallable) {
+  move_only_function<int(int, int)> Add = [](int X, int Y) { return X + Y; };
+  auto Add1 = bind_front(std::move(Add), 1);
+  EXPECT_EQ(Add1(2), 3);
+}
+
+TEST(BindTest, BindsMemberFunctionPointer) {
+  Counter C;
+  auto AddToC = bind_front(&Counter::add, &C);
+  AddToC(2);
+  AddToC(3);
+  EXPECT_EQ(C.Value, 5);
+}
+
+TEST(BindTest, BindsDataMemberPointer) {
+  Counter C;
+  C.Value = 7;
+  auto ValueOf = bind_front(&Counter::Value);
+  EXPECT_EQ(ValueOf(C), 7);
+}
+
+TEST(BindTest, BindsFunctionAsBoundArgument) {
+  auto Apply =
+      bind_front([](int Op(int), int Arg) { return Op(Arg); }, increment);
+  EXPECT_EQ(Apply(1), 2);
+}
+
+// Storage of the callable and bound arguments.
+
+TEST(BindTest, BoundRValueIsMovedIn) {
   OpCounter<>::reset();
   {
-    auto B = bind_front([](OpCounter<> &O, int) {}, OpCounter<>());
+    auto B = bind_front([](OpCounter<> &, int) {}, OpCounter<>());
     B(0);
   }
   EXPECT_EQ(OpCounter<>::defaultConstructions(), 1U);
@@ -59,11 +145,11 @@ TEST(BindTest, MinimalMoves) {
   EXPECT_EQ(OpCounter<>::destructions(), 2U);
 }
 
-TEST(BindTest, MinimalCopies) {
+TEST(BindTest, BoundLValueIsCopiedIn) {
   OpCounter<>::reset();
   {
     OpCounter<> O;
-    auto B = bind_front([](OpCounter<> &O, int) {}, O);
+    auto B = bind_front([](OpCounter<> &, int) {}, O);
     B(0);
   }
   EXPECT_EQ(OpCounter<>::defaultConstructions(), 1U);
@@ -72,21 +158,161 @@ TEST(BindTest, MinimalCopies) {
   EXPECT_EQ(OpCounter<>::destructions(), 2U);
 }
 
-TEST(BindTest, ForwardUnboundArgs) {
-  auto B = bind_front([](int &) {});
-  int N = 7;
-  B(N);
+TEST(BindTest, BoundArgumentsPersistAcrossCalls) {
+  auto Count = bind_front([](int &N) { return ++N; }, 0);
+  EXPECT_EQ(Count(), 1);
+  EXPECT_EQ(Count(), 2);
 }
 
-static int increment(int N) { return N + 1; }
+TEST(BindTest, CopiesAreIndependent) {
+  auto Count = bind_front([](int &N) { return ++N; }, 0);
+  EXPECT_EQ(Count(), 1);
 
-TEST(BindTest, BindFunction) {
-  auto Op = bind_front([](int op(int), int arg) { return op(arg); }, increment);
-  EXPECT_EQ(Op(1), 2);
+  auto Copy = Count;
+  EXPECT_EQ(Copy(), 2);
+  EXPECT_EQ(Count(), 2);
 }
 
-TEST(BindTest, BindTo_move_only_function) {
-  move_only_function<int(int, int)> Add = [](int X, int Y) { return X + Y; };
-  auto Add1 = bind_front(std::move(Add), 1);
-  EXPECT_EQ(Add1(2), 3);
+TEST(BindTest, WrapperCanBeBound) {
+  auto Digits = [](int X, int Y, int Z) { return X * 100 + Y * 10 + Z; };
+  auto Inner = bind_front(Digits, 1);
+
+  auto Outer = bind_front(Inner, 2);
+  EXPECT_EQ(Outer(3), 123);
+
+  auto Rewrapped = bind_front(Inner);
+  EXPECT_EQ(Rewrapped(2, 3), 123);
+}
+
+TEST(BindTest, MoveOnlyBoundArgumentMakesWrapperMoveOnly) {
+  auto B = bind_front([](std::unique_ptr<int> &P) { return *P; },
+                      std::make_unique<int>(42));
+  using WrapperT = decltype(B);
+  EXPECT_FALSE((std::is_copy_constructible_v<WrapperT>));
+  EXPECT_FALSE((std::is_constructible_v<WrapperT, WrapperT &>));
+  EXPECT_TRUE((std::is_move_constructible_v<WrapperT>));
+  EXPECT_EQ(B(), 42);
+}
+
+// Forwarding into the call.
+
+TEST(BindTest, FreeArgumentsAreForwarded) {
+  auto B = bind_front(CategoryOfArg());
+  int N = 0;
+  const int CN = 0;
+  EXPECT_EQ(B(N), Category::LValue);
+  EXPECT_EQ(B(CN), Category::ConstLValue);
+  EXPECT_EQ(B(std::move(N)), Category::RValue);
+  EXPECT_EQ(B(std::move(CN)), Category::ConstRValue);
+}
+
+TEST(BindTest, MoveOnlyFreeArgument) {
+  auto Deref = bind_front([](std::unique_ptr<int> P) { return *P; });
+  EXPECT_EQ(Deref(std::make_unique<int>(42)), 42);
+}
+
+TEST(BindTest, ReferenceReturnIsPreserved) {
+  auto Ref = bind_front([](int &N) -> int & { return N; }, 5);
+  EXPECT_TRUE((std::is_same_v<decltype(Ref()), int &>));
+
+  Ref() = 7;
+  EXPECT_EQ(Ref(), 7);
+}
+
+TEST(BindTest, CallableSeesWrapperCategory) {
+  auto B = bind_front(CategoryOfCall());
+  EXPECT_EQ(B(), Category::LValue);
+  EXPECT_EQ(std::as_const(B)(), Category::ConstLValue);
+  EXPECT_EQ(std::move(B)(), Category::RValue);
+  EXPECT_EQ(std::move(std::as_const(B))(), Category::ConstRValue);
+}
+
+TEST(BindTest, BoundArgumentsSeeWrapperCategory) {
+  auto B = bind_front(CategoryOfArg(), 0);
+  EXPECT_EQ(B(), Category::LValue);
+  EXPECT_EQ(std::as_const(B)(), Category::ConstLValue);
+  EXPECT_EQ(std::move(B)(), Category::RValue);
+  EXPECT_EQ(std::move(std::as_const(B))(), Category::ConstRValue);
+}
+
+TEST(BindTest, RValueCallCanConsumeBoundArguments) {
+  auto Take = bind_front([](std::unique_ptr<int> P) { return *P; },
+                         std::make_unique<int>(42));
+  EXPECT_FALSE((std::is_invocable_v<decltype(Take) &>));
+  EXPECT_EQ(std::move(Take)(), 42);
+}
+
+TEST(BindTest, ConstCallRequiresConstCallable) {
+  auto Count = bind_front([N = 0]() mutable { return ++N; });
+  EXPECT_TRUE((std::is_invocable_v<decltype(Count) &>));
+  EXPECT_FALSE((std::is_invocable_v<const decltype(Count) &>));
 }
+
+// A call the callable rejects for the wrapper's category must not fall back to
+// another category's overload, even one that would accept it.
+
+TEST(BindTest, NonConstCallDoesNotFallBackToConst) {
+  struct ConstOnly {
+    void operator()() & = delete;
+    void operator()() const & {}
+  };
+  auto B = bind_front(ConstOnly());
+  EXPECT_FALSE((std::is_invocable_v<decltype(B) &>));
+  EXPECT_TRUE((std::is_invocable_v<const decltype(B) &>));
+}
+
+TEST(BindTest, RValueCallDoesNotFallBackToConst) {
+  struct LValueOnly {
+    void operator()() && = delete;
+    void operator()() const & {}
+  };
+  auto B = bind_front(LValueOnly());
+  EXPECT_FALSE((std::is_invocable_v<decltype(B) &&>));
+  EXPECT_TRUE((std::is_invocable_v<decltype(B) &>));
+}
+
+// noexcept.
+
+TEST(BindTest, NoexceptIsPropagated) {
+  auto Nothrow = bind_front(noexceptAdd, 41);
+  EXPECT_TRUE((std::is_nothrow_invocable_v<decltype(Nothrow), int>));
+
+  auto MayThrow = bind_front(addInts, 41);
+  EXPECT_FALSE((std::is_nothrow_invocable_v<decltype(MayThrow), int>));
+}
+
+TEST(BindTest, NoexceptFollowsTheSelectedOverload) {
+  struct NoexceptWhenConst {
+    void operator()() & {}
+    void operator()() const & noexcept {}
+  };
+  auto B = bind_front(NoexceptWhenConst());
+  EXPECT_FALSE((std::is_nothrow_invocable_v<decltype(B) &>));
+  EXPECT_TRUE((std::is_nothrow_invocable_v<const decltype(B) &>));
+}
+
+TEST(BindTest, CopyAndMoveNoexceptFollowState) {
+  using IntB = decltype(bind_front(addInts, 1));
+  EXPECT_TRUE((std::is_nothrow_copy_constructible_v<IntB>));
+  EXPECT_TRUE((std::is_nothrow_move_constructible_v<IntB>));
+
+  auto Ignore = [](const MayThrowOnCopy &) {};
+  using MayThrowB = decltype(bind_front(Ignore, MayThrowOnCopy()));
+  EXPECT_FALSE((std::is_nothrow_copy_constructible_v<MayThrowB>));
+  EXPECT_FALSE((std::is_nothrow_move_constructible_v<MayThrowB>));
+}
+
+#if ORC_RT_ENABLE_EXCEPTIONS
+
+// A throwing copy of a bound argument must reach the caller rather than
+// terminating the program.
+TEST(BindTest, ExceptionFromBoundArgumentCopyPropagates) {
+  struct ThrowsOnCopy {
+    ThrowsOnCopy() = default;
+    ThrowsOnCopy(const ThrowsOnCopy &) { throw 42; }
+  };
+  ThrowsOnCopy T;
+  EXPECT_THROW((void)bind_front([](ThrowsOnCopy &) {}, T), int);
+}
+
+#endif // ORC_RT_ENABLE_EXCEPTIONS



More information about the llvm-commits mailing list