[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