Skip to content

Commit c9e0184

Browse files
Remove fragile can_be_nullptr trait that broke create_service() (#1742)
The two set() overloads in AnyServiceCallback and GenericServiceCallback were selected by detail::can_be_nullptr, whose non-QNX specialization probed the raw callable with decltype(std::declval<T&>() = nullptr). That expression is ill-formed for some callables/compilers, disabling both overloads and breaking compilation of create_service() (and the generic service) for affected callbacks. Replace the trait with a single set() that stores the callback and then checks the resulting std::function for emptiness via detail::callback_is_null(), which visits the variant. std::function's operator bool flags only genuine null targets (e.g. a null function pointer); lambdas and bind expressions are never empty, so behavior is preserved while the brittle SFINAE is removed. This also collapses the duplicated overload pair in both headers. Add regression tests for function-pointer and std::function callbacks (which previously failed to compile) and null-rejection for both. Fixes #1742 Generated-by: Claude Opus 4.8 (Anthropic) Signed-off-by: Pavel Guzenfeld <pavelguzenfeld@gmail.com>
1 parent 7321b44 commit c9e0184

3 files changed

Lines changed: 72 additions & 106 deletions

File tree

rclcpp/include/rclcpp/any_service_callback.hpp

Lines changed: 24 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,27 @@ namespace rclcpp
3333

3434
namespace detail
3535
{
36-
template<typename T, typename = void>
37-
struct can_be_nullptr : std::false_type {};
38-
39-
// Some lambdas define a comparison with nullptr,
40-
// but we see a warning that they can never be null when using it.
41-
// We also test if `T &` can be assigned to `nullptr` to avoid the issue.
42-
template<typename T>
43-
#ifdef __QNXNTO__
44-
struct can_be_nullptr<T, std::void_t<
45-
decltype(std::declval<T>() == nullptr)>>: std::true_type {};
46-
#else
47-
struct can_be_nullptr<T, std::void_t<
48-
decltype(std::declval<T>() == nullptr), decltype(std::declval<T &>() = nullptr)>>
49-
: std::true_type {};
50-
#endif
36+
// Returns true if the callback stored in the given variant is empty/null.
37+
// The stored alternatives are std::function objects, whose operator bool is
38+
// false only when empty; lambdas and bind expressions are never empty, so this
39+
// flags only genuine null targets (e.g. a null function pointer). Checking the
40+
// stored std::function avoids probing the raw callable type with `== nullptr` /
41+
// `= nullptr`, which is ill-formed for some callables and broke compilation of
42+
// create_service() with certain callbacks/compilers (see ros2/rclcpp#1742).
43+
template<typename VariantT>
44+
bool
45+
callback_is_null(const VariantT & callback)
46+
{
47+
return std::visit(
48+
[](const auto & cb) {
49+
if constexpr (std::is_same_v<std::decay_t<decltype(cb)>, std::monostate>) {
50+
return true;
51+
} else {
52+
return !cb;
53+
}
54+
},
55+
callback);
56+
}
5157
} // namespace detail
5258

5359
// Forward declare
@@ -62,9 +68,7 @@ class AnyServiceCallback
6268
: callback_(std::monostate{})
6369
{}
6470

65-
template<
66-
typename CallbackT,
67-
typename std::enable_if_t<!detail::can_be_nullptr<CallbackT>::value, int> = 0>
71+
template<typename CallbackT>
6872
void
6973
set(CallbackT && callback)
7074
{
@@ -102,51 +106,11 @@ class AnyServiceCallback
102106
// of all the above workaround ...
103107
callback_ = std::forward<CallbackT>(callback);
104108
}
105-
}
106109

107-
template<
108-
typename CallbackT,
109-
typename std::enable_if_t<detail::can_be_nullptr<CallbackT>::value, int> = 0>
110-
void
111-
set(CallbackT && callback)
112-
{
113-
if (!callback) {
110+
// Reject a null target (e.g. a null function pointer or empty std::function).
111+
if (detail::callback_is_null(callback_)) {
114112
throw std::invalid_argument("AnyServiceCallback::set(): callback cannot be nullptr");
115113
}
116-
// Workaround Windows issue with std::bind
117-
if constexpr (
118-
rclcpp::function_traits::same_arguments<
119-
CallbackT,
120-
SharedPtrCallback
121-
>::value)
122-
{
123-
callback_.template emplace<SharedPtrCallback>(callback);
124-
} else if constexpr ( // NOLINT
125-
rclcpp::function_traits::same_arguments<
126-
CallbackT,
127-
SharedPtrWithRequestHeaderCallback
128-
>::value)
129-
{
130-
callback_.template emplace<SharedPtrWithRequestHeaderCallback>(callback);
131-
} else if constexpr ( // NOLINT
132-
rclcpp::function_traits::same_arguments<
133-
CallbackT,
134-
SharedPtrDeferResponseCallback
135-
>::value)
136-
{
137-
callback_.template emplace<SharedPtrDeferResponseCallback>(callback);
138-
} else if constexpr ( // NOLINT
139-
rclcpp::function_traits::same_arguments<
140-
CallbackT,
141-
SharedPtrDeferResponseCallbackWithServiceHandle
142-
>::value)
143-
{
144-
callback_.template emplace<SharedPtrDeferResponseCallbackWithServiceHandle>(callback);
145-
} else {
146-
// the else clause is not needed, but anyways we should only be doing this instead
147-
// of all the above workaround ...
148-
callback_ = std::forward<CallbackT>(callback);
149-
}
150114
}
151115

152116
// template<typename Allocator = std::allocator<typename ServiceT::Response>>

rclcpp/include/rclcpp/generic_service.hpp

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <utility>
2424
#include <variant>
2525

26+
#include "rclcpp/any_service_callback.hpp"
2627
#include "rclcpp/typesupport_helpers.hpp"
2728

2829
#include "rosidl_runtime_c/service_type_support_struct.h"
@@ -45,9 +46,7 @@ class GenericServiceCallback
4546
: callback_(std::monostate{})
4647
{}
4748

48-
template<
49-
typename CallbackT,
50-
typename std::enable_if_t<!detail::can_be_nullptr<CallbackT>::value, int> = 0>
49+
template<typename CallbackT>
5150
void
5251
set(CallbackT && callback)
5352
{
@@ -85,50 +84,10 @@ class GenericServiceCallback
8584
// of all the above workaround ...
8685
callback_ = std::forward<CallbackT>(callback);
8786
}
88-
}
8987

90-
template<
91-
typename CallbackT,
92-
typename std::enable_if_t<detail::can_be_nullptr<CallbackT>::value, int> = 0>
93-
void
94-
set(CallbackT && callback)
95-
{
96-
if (!callback) {
97-
throw std::invalid_argument("AnyServiceCallback::set(): callback cannot be nullptr");
98-
}
99-
// Workaround Windows issue with std::bind
100-
if constexpr (
101-
rclcpp::function_traits::same_arguments<
102-
CallbackT,
103-
SharedPtrCallback
104-
>::value)
105-
{
106-
callback_.template emplace<SharedPtrCallback>(callback);
107-
} else if constexpr ( // NOLINT
108-
rclcpp::function_traits::same_arguments<
109-
CallbackT,
110-
SharedPtrWithRequestHeaderCallback
111-
>::value)
112-
{
113-
callback_.template emplace<SharedPtrWithRequestHeaderCallback>(callback);
114-
} else if constexpr ( // NOLINT
115-
rclcpp::function_traits::same_arguments<
116-
CallbackT,
117-
SharedPtrDeferResponseCallback
118-
>::value)
119-
{
120-
callback_.template emplace<SharedPtrDeferResponseCallback>(callback);
121-
} else if constexpr ( // NOLINT
122-
rclcpp::function_traits::same_arguments<
123-
CallbackT,
124-
SharedPtrDeferResponseCallbackWithServiceHandle
125-
>::value)
126-
{
127-
callback_.template emplace<SharedPtrDeferResponseCallbackWithServiceHandle>(callback);
128-
} else {
129-
// the else clause is not needed, but anyways we should only be doing this instead
130-
// of all the above workaround ...
131-
callback_ = std::forward<CallbackT>(callback);
88+
// Reject a null target (e.g. a null function pointer or empty std::function).
89+
if (detail::callback_is_null(callback_)) {
90+
throw std::invalid_argument("GenericServiceCallback::set(): callback cannot be nullptr");
13291
}
13392
}
13493

rclcpp/test/rclcpp/test_any_service_callback.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,46 @@ TEST_F(TestAnyServiceCallback, set_and_dispatch_defered_with_service_handle) {
109109
EXPECT_EQ(nullptr, any_service_callback_.dispatch(nullptr, request_header_, request_)));
110110
EXPECT_EQ(callback_with_header_calls, 1);
111111
}
112+
113+
namespace
114+
{
115+
void free_service_callback(
116+
const std::shared_ptr<test_msgs::srv::Empty::Request>,
117+
std::shared_ptr<test_msgs::srv::Empty::Response>)
118+
{}
119+
} // namespace
120+
121+
// Regression for ros2/rclcpp#1742: setting the callback with a plain function
122+
// pointer or std::function must compile and work. The previous can_be_nullptr
123+
// SFINAE trait was ill-formed for some callables and broke create_service().
124+
TEST_F(TestAnyServiceCallback, set_and_dispatch_function_pointer) {
125+
void (* callback)(
126+
const std::shared_ptr<test_msgs::srv::Empty::Request>,
127+
std::shared_ptr<test_msgs::srv::Empty::Response>) = &free_service_callback;
128+
EXPECT_NO_THROW(any_service_callback_.set(callback));
129+
EXPECT_NO_THROW(
130+
EXPECT_NE(nullptr, any_service_callback_.dispatch(nullptr, request_header_, request_)));
131+
}
132+
133+
TEST_F(TestAnyServiceCallback, set_and_dispatch_std_function) {
134+
std::function<void(
135+
const std::shared_ptr<test_msgs::srv::Empty::Request>,
136+
std::shared_ptr<test_msgs::srv::Empty::Response>)> callback = free_service_callback;
137+
EXPECT_NO_THROW(any_service_callback_.set(callback));
138+
EXPECT_NO_THROW(
139+
EXPECT_NE(nullptr, any_service_callback_.dispatch(nullptr, request_header_, request_)));
140+
}
141+
142+
TEST_F(TestAnyServiceCallback, set_null_function_pointer_throws) {
143+
void (* callback)(
144+
const std::shared_ptr<test_msgs::srv::Empty::Request>,
145+
std::shared_ptr<test_msgs::srv::Empty::Response>) = nullptr;
146+
EXPECT_THROW(any_service_callback_.set(callback), std::invalid_argument);
147+
}
148+
149+
TEST_F(TestAnyServiceCallback, set_null_std_function_throws) {
150+
std::function<void(
151+
const std::shared_ptr<test_msgs::srv::Empty::Request>,
152+
std::shared_ptr<test_msgs::srv::Empty::Response>)> callback = nullptr;
153+
EXPECT_THROW(any_service_callback_.set(callback), std::invalid_argument);
154+
}

0 commit comments

Comments
 (0)