Skip to content

Add const reference service callback signatures - #3269

Open
KR-Ravindra wants to merge 1 commit into
ros2:rollingfrom
KR-Ravindra:feat/service-callback-const-ref
Open

KR-Ravindra wants to merge 1 commit into
ros2:rollingfrom
KR-Ravindra:feat/service-callback-const-ref

Conversation

@KR-Ravindra

@KR-Ravindra KR-Ravindra commented Sep 13, 2026

Copy link
Copy Markdown

Problem

AnySubscriptionCallback accepts const MessageT & callbacks, but AnyServiceCallback only accepts std::shared_ptr signatures. Every service callback therefore has to take shared_ptrs (the response one is copied per call) even when the handler answers immediately and never needs to extend the request/response lifetime. This is inconsistent with subscriptions.

Design

Follows the approach suggested in the issue: the request and response keep arriving as shared_ptr internally; dispatch() dereferences them for the user callback, exactly like AnySubscriptionCallback does for const MessageT &. The existing shared_ptr overloads are unchanged.

Fix

rclcpp/include/rclcpp/any_service_callback.hpp

  • New variant alternatives ConstRefCallback = void (const Request &, Response &) and ConstRefWithRequestHeaderCallback = void (const rmw_request_id_t &, const Request &, Response &), appended after the existing alternatives.
  • Both set() overloads get matching function_traits::same_arguments branches so std::bind results resolve like the shared_ptr shapes do.
  • dispatch() calls cb(*request, *response) / cb(*request_header, *request, *response) and returns the response as before.

The new alternatives cannot be ambiguous with the existing ones in the std::variant converting assignment: a callable taking const Request & is not invocable with a shared_ptr<Request> and vice versa, so at most one std::function alternative is viable for any callback.

How tested

rclcpp/test/rclcpp/test_any_service_callback.cpp: four new cases (const-ref without/with request header, response mutation propagated to the returned shared_ptr, std::bind for both shapes). rclcpp/test/rclcpp/test_service.cpp: create_service<> with a const-ref callback, exercised end to end through a client.

Before the change, the new callback shapes fail to compile:

any_service_callback.hpp:102:17: error: no match for 'operator=' (operand types are
'std::variant<std::monostate, std::function<void(std::shared_ptr<...Request>, std::shared_ptr<...Response>)>, ...>'
and '<lambda(const ...Request&, ...Response&)>')

After the change the header compiles and all cases pass. I could not run colcon test locally (no ROS 2 workspace), so the new gtest cases were mirrored in a standalone gtest harness that compiles the unmodified any_service_callback.hpp / function_traits.hpp against stub rmw/tracetools headers: 10/10 pass with -Wall -Wextra on GCC 13, including the pre-existing callback shapes. cpplint with the ament filters reports no issues. Please rely on CI for the full build and for ament_uncrustify.

Links

This change was prepared with an AI agent operated by KR-Ravindra, who reviewed and tested it.

@KR-Ravindra

Copy link
Copy Markdown
Author

Round 1 self-review.

Verified against rolling (e47c084):

  1. The two new alternatives are appended after the four existing ones in AnyServiceCallback's variant; the existing shared_ptr shapes and their set() branches are untouched.
  2. Ambiguity: a callable taking const Request & is not invocable with shared_ptr<Request> and vice versa, so the std::variant converting assignment sees at most one viable alternative; generic [](auto, auto) lambdas were already ambiguous before this change.
  3. dispatch() dereferences request_header only in the with-header branch; Executor::execute_service always passes a non-null rmw_request_id_t and Service::handle_request already dereferences it.
  4. GenericService keeps its own callback variant and is intentionally not changed here.
  5. Corrected one sentence in the description: upstream dispatch() moves the request and copies only the response shared_ptr.

No duplicate: #3078 cross-references only this PR, and the approach is the one jmachowinski suggested there. Open #3168 touches the same set() overloads, so one of the two will need a rebase. DCO passes; the abi workflow needs a maintainer to approve the run. Marking ready.

@KR-Ravindra
KR-Ravindra marked this pull request as ready for review September 13, 2026 06:56

@fujitatomoya fujitatomoya left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm with green CI. monor comment does not block this PR.

Comment on lines +213 to +218
} else if (std::holds_alternative<ConstRefCallback>(callback_)) {
const auto & cb = std::get<ConstRefCallback>(callback_);
cb(*request, *response);
} else if (std::holds_alternative<ConstRefWithRequestHeaderCallback>(callback_)) {
const auto & cb = std::get<ConstRefWithRequestHeaderCallback>(callback_);
cb(*request_header, *request, *response);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would add assert(request_header) / assert(request) (or a throw std::runtime_error) before dereferencing just in case. this is different situation from shared pointer cases above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fujitatomoya done in a90d9ca: dispatch() now throws std::runtime_error before dereferencing a null request or request_header in the two const-ref branches, matching the unset-callback check above, and test_any_service_callback.cpp gets a test for the null cases. Sorry this lands after you launched CI; the change is limited to those two branches. Could you take another look?

@fujitatomoya

Copy link
Copy Markdown
Collaborator

Pulls: #3269
Gist: https://gist.githubusercontent.com/fujitatomoya/49854ee45d8c5672d85cb581931e8c19/raw/43ceaeb52aaf47a9f24252885bbf373774819052/ros2.repos
BUILD args: --packages-above-and-dependencies rclcpp
TEST args: --packages-above rclcpp
ROS Distro: rolling
Job: ci_launcher
ci_launcher ran: https://ci.ros2.org/job/ci_launcher/20464

  • Linux Build Status
  • Linux-aarch64 Build Status
  • Linux-rhel Build Status
  • Windows Build Status

AnyServiceCallback now also accepts
  void (const Request &, Response &)
  void (const rmw_request_id_t &, const Request &, Response &)
in addition to the existing shared_ptr signatures. The request and
response are still received as shared_ptr internally and are
dereferenced before calling the user callback, mirroring what
AnySubscriptionCallback does for const MessageT & callbacks.
dispatch() throws std::runtime_error instead of dereferencing a null
request or request header for these two alternatives.

Both set() overloads gain matching function_traits::same_arguments
branches so std::bind results resolve to the right alternative, and
dispatch() handles the two new alternatives. Existing overloads are
unchanged.

Signed-off-by: KR Ravindra <42912207+KR-Ravindra@users.noreply.github.com>
@KR-Ravindra
KR-Ravindra force-pushed the feat/service-callback-const-ref branch from e2c22d4 to a90d9ca Compare September 16, 2026 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants