Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C++ API

The nano-ros C++ API is a freestanding C++14 interface for ROS 2 nodes on embedded and host targets. It needs no standard library, no exceptions and no RTTI, so the same code builds for Zephyr, FreeRTOS, NuttX, ThreadX, bare metal and Linux.

  • Upstream’s names, in upstream’s namespaces. rclcpp::Node, Publisher<M>, Subscription<M>, Service<S>, Client<S>, Timer, GuardCondition, Executor, and rclcpp_action::Server<A> / Client<A>. Every user-facing name is in rclcpp::, rclcpp_action:: or rclcpp_lifecycle::; there is no nros:: namespace. The extensions with no upstream counterpart (the Poll* types, the *Storage types, FixedString, …) are in rclcpp:: too.
  • Errors are values. Every fallible call returns rclcpp::Result, and RCLCPP_TRY returns early on failure. Nothing throws.
  • Generated message types. std_msgs::msg::Int32, example_interfaces::srv::AddTwoInts and so on, generated by the build from the packages your node depends on (see Message Generation).
  • An opt-in std surface for ported code. Defining NROS_CPP_STD adds the std::string / std::function / std::chrono overloads and the std::shared_ptr-returning rclcpp::Node factories. The build requests it; it is never detected from the toolchain. See Two surfaces.

The headers live in the SDK’s C++ include directory at upstream’s paths: rclcpp/, rclcpp_action/ and rclcpp_lifecycle/. rclcpp/detail/ is internal.

To build and run a first C++ node, start with First Node — C++; to grow it into a workspace, see C++ in a workspace. To bring an existing ROS 2 node across, see Porting a ROS 2 C++ node. This page is the API itself.

API reference

C++ API Doxygen — start here.

The Doxygen site is generated from the current main branch. The umbrella header rclcpp/rclcpp.hpp pulls in every public surface a user application needs.

Where to start

  • rclcpp/rclcpp.hpp — convenience umbrella include
  • rclcpp::init_in / rclcpp::shutdown — session lifetime
  • rclcpp::Node — node + create_publisher/subscription/service/client/action_*
  • rclcpp::Publisher<M> / Subscription<M>. Hosted, a subscription callback may also take upstream’s const rclcpp::MessageInfo& as its second argument (see Subscription)
  • rclcpp::Service<S> / Client<S> — the DISPATCH service server and client. A handler is registered into the executor arena, which owns the entity. Service<S>::SharedPtr is rclcpp::ServiceHandle<S>, a two-word keep-alive with nothing to dereference; Client<S>::SharedPtr is rclcpp::ClientHandle<S>, the same two words with one verb, async_send_request. Neither has an operator->.
  • rclcpp::PollService<S> — L1 polling service server. The caller owns the server and drains it with take_request / send_response from its own spin loop. node.create_service<S>(name) with no handler hands one back.
  • rclcpp::PollClient<S> — L1 future-style service client, the client half of the same split. The caller owns the client and drains the reply itself: send_request returns a rclcpp::Future<T> (on a hosted build async_send_request(shared_ptr) returns upstream’s FutureAndRequestId — .future, .request_id, share()), call blocks while driving the executor, call_polling blocks without driving it, and wait_for_service / service_is_ready probe the server. node.create_client<S>(name) with no handler hands one back.
  • rclcpp_action::Server<A> / Client<A> — L2 callback model (executor-arena registered). rclcpp::ActionServer<A> / rclcpp::ActionClient<A> are the same two types. Hosted, upstream’s goal handles too (see Actions with upstream’s goal handles).
  • rclcpp::PollingActionServer<A> / PollingActionClient<A> — L1 polling model. Caller drives take_* / accept_goal / complete_goal from a spin loop.
  • rclcpp::Future<T> — async result handle
  • rclcpp::Executor, Timer, GuardCondition
  • Parameters are methods ON the node — declare_parameter<T> / get_parameter<T> / set_parameter<T> / has_parameter, upstream’s names, forwarding to the one store the rcl_interfaces services read. rclcpp::Seq<T, N> is the array VALUE a freestanding node declares with (std::vector<T> under NROS_CPP_STD). There is no standalone rclcpp::ParameterServer: a second store would be invisible to ros2 param get. See Differences from ROS 2 §9 for what is intentionally smaller than rclcpp, and Parameters below.

Two-layer API. The rclcpp::ActionServer<A> / ActionClient<A> templates are L2 callback handles, dispatched by the executor. The rclcpp::PollingActionServer<A> / PollingActionClient<A> are L1 polling templates with the same typed serialization glue but caller-driven control flow. Subscriptions, services and service clients have both shapes too (Subscription / PollSubscription, and so on). See Two-Layer API for the per-layer rationale.

Event-driven wake callbacks. PollingActionServer<A> and PollingActionClient<A> expose a nested WakeState POD and typed set_{goal,cancel,get_result,goal_response, cancel_response,result,feedback}_wake_callback(state, cb, ctx) methods. Pair an nros_cpp_wake_state_t with each entity / channel to wake on rx instead of polling.

Core API

The snippets below use the freestanding surface. Each create_* writes the entity through an out-reference and returns a Result.

Initialization

#include <rclcpp/rclcpp.hpp>

// Global init (simple applications). `init_in` is the Result-returning form:
// upstream's `rclcpp::init(argc, argv)` returns `void`, so the name differs
// where the contract does.
rclcpp::Result ret = rclcpp::init_in("tcp/127.0.0.1:7447", 0);

// Or an explicit executor (multi-executor patterns)
rclcpp::Executor executor;
RCLCPP_TRY(rclcpp::Executor::create(executor, "tcp/127.0.0.1:7447"));

Node

rclcpp::Node node;
RCLCPP_TRY(rclcpp::create_node(node, "my_node"));

// Or with an explicit executor:
RCLCPP_TRY(executor.create_node(node, "my_node", "/namespace"));

Publisher

#include "std_msgs.hpp"

rclcpp::Publisher<std_msgs::msg::Int32> pub;
RCLCPP_TRY(node.create_publisher(pub, "/chatter"));

std_msgs::msg::Int32 msg;
msg.data = 42;
RCLCPP_TRY(pub.publish(msg));

A publisher can be marked express (it bypasses transmit batching) through its QoS profile; see TX Throughput & Latency Tuning.

Subscription

A poll-style subscription is rclcpp::PollSubscription<M>: the caller owns the subscriber, so the caller is the one that takes from it. Call spin_once() to drive I/O, then take() to check for a message. rclcpp::Subscription<M> is the DISPATCH subscription, whose samples the executor arena delivers to a callback.

rclcpp::PollSubscription<std_msgs::msg::Int32> sub;
RCLCPP_TRY(node.create_subscription(sub, "/chatter"));

rclcpp::spin_once(100);

std_msgs::msg::Int32 msg;
if (sub.take(msg).ok()) {
    printf("Received: %d\n", msg.data);
}

On a hosted build (NROS_CPP_STD) a dispatch subscription’s callback may take upstream’s const rclcpp::MessageInfo& as a second argument, after the message in any of its four forms (const M&, std::shared_ptr<const M>, std::shared_ptr<M>, std::unique_ptr<M>):

auto sub = node->create_subscription<std_msgs::msg::Int32>(
    "/chatter", 10, [](const std_msgs::msg::Int32& msg, const rclcpp::MessageInfo& info) {
        const auto& r = info.get_rmw_message_info();
        printf("%d from seq %llu at %lld ns\n", msg.data,
               (unsigned long long)r.publication_sequence_number, (long long)r.source_timestamp);
    });

get_rmw_message_info() carries source_timestamp, received_timestamp, publication_sequence_number, reception_sequence_number and the 24-byte publisher_gid, as the backend reported them. A backend that reports none (XRCE, uORB) leaves every field zero. from_intra_process is always false. A freestanding build refuses the shape at compile time: the metadata path keeps the callable on the heap. SubscriptionOptions::message_info = true with a callback that takes no MessageInfo aborts the create, because it would otherwise configure nothing.

Service server

rclcpp::PollService<S> is the polled server: the caller owns it and drains it. rclcpp::Service<S> is the DISPATCH server, which takes a handler.

#include "example_interfaces.hpp"

using AddTwoInts = example_interfaces::srv::AddTwoInts;

rclcpp::PollService<AddTwoInts> srv;
RCLCPP_TRY(node.create_service(srv, "/add_two_ints"));

// In your main loop:
rclcpp::spin_once(10);
AddTwoInts::Request req;
int64_t seq;
if (srv.take_request(req, seq).ok()) {
    AddTwoInts::Response resp;
    resp.sum = req.a + req.b;
    srv.send_response(seq, resp);
}

Service client

rclcpp::PollClient<S> is the polled client: the caller owns it and drains the reply. rclcpp::Client<S> is the DISPATCH client, whose one verb is async_send_request and whose reply reaches a handler during spin_once.

rclcpp::PollClient<AddTwoInts> client;
RCLCPP_TRY(node.create_client(client, "/add_two_ints"));

AddTwoInts::Request req;
req.a = 1; req.b = 2;
AddTwoInts::Response resp;
RCLCPP_TRY(client.call(req, resp));
// resp.sum == 3

Action server

rclcpp_action::Server<A> is a DISPATCH server: the executor answers goal and cancel requests through callbacks you install, and the goal itself is driven through the server by its 16-byte id. Upstream hands handle_accepted an owning std::shared_ptr<ServerGoalHandle>; here the goals live in the server’s static arena, so the callback receives the id instead. A polled form, where the caller takes each goal request itself, is rclcpp::PollingActionServer<A>.

#include "example_interfaces.hpp"

using Fibonacci = example_interfaces::action::Fibonacci;

rclcpp_action::Server<Fibonacci> srv;
RCLCPP_TRY(node.create_action_server(srv, "/fibonacci"));

// Stateless callables: an empty-capture lambda or a plain function pointer.
// The `*_with_ctx` forms add a `void*` for state.
RCLCPP_TRY(srv.set_goal_callback([](const uint8_t id[16], const Fibonacci::Goal& goal) {
    return goal.order > 0 ? rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE
                          : rclcpp_action::GoalResponse::REJECT;
}));
RCLCPP_TRY(srv.set_accepted_callback([](const uint8_t id[16]) {
    // Record `id`; the work happens outside the callback, which must return
    // promptly.
}));

// Later, from the main loop, for an accepted goal `goal_id`:
Fibonacci::Feedback fb;
// ... fill feedback ...
srv.publish_feedback(goal_id, fb);

Fibonacci::Result result;
// ... fill result ...
srv.complete_goal(goal_id, result);  // GoalStatus::Succeeded; another overload takes a status

Action client

rclcpp_action::Client<A> keeps its goal, feedback and result buffers (and the four transport channels underneath) in fixed-size storage inside the Client<A> instance itself. Nothing is heap-allocated per send_goal call, and the type is move-only.

Blocking convenience. send_goal() and get_result() spin the executor internally until the server replies or the per-call timeout expires:

rclcpp_action::Client<Fibonacci> client;
RCLCPP_TRY(node.create_action_client(client, "/fibonacci"));

Fibonacci::Goal goal;
goal.order = 10;
uint8_t goal_id[16];
RCLCPP_TRY(client.send_goal(goal, goal_id));

Fibonacci::Result result;
RCLCPP_TRY(client.get_result(goal_id, result));

These helpers are sugar over send_goal_async() + spin_once(). They cannot be called from inside a dispatch callback; a blocking call made there fails with ErrorCode::Reentrant.

Future-based async. send_goal_future() / get_result_future() return a Future<T> keyed by its goal: each future receives only its own goal’s acceptance or result, whether the executor dispatched it during spin_once or the future’s own poll took it, and a result for a goal nobody waits on is dropped. At most MAX_CONCURRENT_GOALS of each may be pending per client; past that the send is refused and the future is empty (valid() false):

auto fut = client.send_goal_future(goal);
typename decltype(client)::GoalAccept accept;
RCLCPP_TRY(fut.wait(executor.handle(), 5000, accept));
if (accept.accepted) {
    auto rfut = client.get_result_future(accept.goal_id);
    Fibonacci::Result result;
    RCLCPP_TRY(rfut.wait(executor.handle(), 10000, result));
}

GoalAccept is a nested type on rclcpp_action::Client<A> (16-byte UUID + bool accepted).

Feedback. Feedback is not goal-scoped at the stream layer: feedback_stream() yields FeedbackType across every active goal for this client.

auto& fb_stream = client.feedback_stream();
Fibonacci::Feedback fb;
while (fb_stream.try_next(fb).ok()) { /* ... */ }
// Or blocking:
RCLCPP_TRY(fb_stream.wait_next(executor.handle(), 500, fb));

For per-goal feedback dispatch, use the callback API.

Callback API: SendGoalOptions + set_callbacks(). This is the rclcpp-style entry point. SendGoalOptions is a nested POD on rclcpp_action::Client<A>; populate the three function-pointer fields (goal_response, feedback, result) plus an optional context pointer, then install once. Callbacks fire from spin_once():

typename decltype(client)::SendGoalOptions opts;
opts.goal_response = [](bool accepted, const uint8_t id[16], void* ctx) {
    auto* state = static_cast<MyState*>(ctx);
    state->accepted = accepted;
    std::memcpy(state->goal_id, id, 16);
};
opts.feedback = [](const uint8_t id[16], const uint8_t* data, size_t len, void* ctx) {
    Fibonacci::Feedback fb;
    if (Fibonacci::Feedback::ffi_deserialize(data, len, &fb) == 0) {
        // dispatch on `id`
    }
};
opts.result = [](const uint8_t id[16], int32_t status, const uint8_t* data, size_t len, void* ctx) {
    // status: 4=Succeeded, 5=Canceled, 6=Aborted
};
opts.context = &my_state;
RCLCPP_TRY(client.set_callbacks(opts));

RCLCPP_TRY(client.send_goal_async(goal, goal_id));  // fire-and-forget
while (!my_state.done) { rclcpp::spin_once(10); }

set_callbacks() may be called before or after send_goal_async(): the executor reads the latest callbacks on each dispatch. The client also keeps the most recent feedback and result bytes, so the same Client can drive feedback_stream().try_next() and get_result_future().wait() even with callbacks installed.

Actions with upstream’s goal handles (hosted)

On a hosted build (NROS_CPP_STD) the stock rclcpp_action spellings are drop-ins over the two classes above:

using Fibonacci = example_interfaces::action::Fibonacci;
using GoalHandle = rclcpp_action::ServerGoalHandle<Fibonacci>;
using namespace std::placeholders;

// Server: free factory over the node interfaces, std::bind-ed members.
rclcpp_action::Server<Fibonacci>::SharedPtr server = rclcpp_action::create_server<Fibonacci>(
    get_node_base_interface(), get_node_clock_interface(), get_node_logging_interface(),
    get_node_waitables_interface(), "fibonacci",
    std::bind(&MyNode::handle_goal, this, _1, _2),    // (const GoalUUID&, shared_ptr<const Goal>)
    std::bind(&MyNode::handle_cancel, this, _1),      // (shared_ptr<GoalHandle>)
    std::bind(&MyNode::handle_accepted, this, _1));   // (shared_ptr<GoalHandle>)
// in handle_accepted: goal_handle->get_goal(), ->publish_feedback(fb),
// ->is_canceling(), ->succeed(result) / ->abort(result) / ->canceled(result)

// Client: SendGoalOptions' upstream fields, bound per goal by async_send_goal.
auto client = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");
auto opts = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
opts.goal_response_callback = [](rclcpp_action::ClientGoalHandle<Fibonacci>::SharedPtr h) {};
opts.feedback_callback = [](auto h, const std::shared_ptr<const Fibonacci::Feedback> fb) {};
opts.result_callback = [](const rclcpp_action::Client<Fibonacci>::WrappedResult& r) {
    // r.code: rclcpp_action::ResultCode::SUCCEEDED / ABORTED / CANCELED; r.result
};
auto fut = client->async_send_goal(goal, opts);   // rclcpp::Future<GoalAccept>

// The handle-taking calls return upstream's std::shared_future.
auto result = client->async_get_result(goal_handle);   // shared_future<WrappedResult>
auto cancel = client->async_cancel_goal(goal_handle);  // shared_future<CancelResponse::SharedPtr>
if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS) {
    const auto& wrapped = result.get();
}
// server, after handle_goal returned ACCEPT_AND_DEFER:
goal_handle->execute();   // ACCEPTED -> EXECUTING, status array published

X<A>::SharedPtr is rclcpp::Owned<X<A>> (the entity by value), and the get_node_*_interface() handles name the node and do nothing else. A goal handle is a heap token over the goal id: every call forwards to the server or client keyed by it, and the goal’s state stays in the arena. Drive it from the executor’s task only — not from a thread you started — and do not move the server while a handle is in use. The callbacks run from the executor’s dispatch, beside the client-wide set_callbacks ones. async_send_goal returns the goal-keyed rclcpp::Future<GoalAccept>, not a std::shared_future; get_goal_stamp is absent.

async_get_result / async_cancel_goal / async_cancel_all_goals / async_cancel_goals_before return a rclcpp_action::detail::DrivenSharedFuture<T>, which IS-A std::shared_future<T>: its is_ready() (what spin_until_future_complete polls), wait_for and get drive the client’s dispatch and executor while they wait, since no other thread fulfils it. Converted to a bare std::shared_future it still completes, but only when something else drives the client. Every async_get_result for one goal returns the handle’s own future; a result-aware goal’s handle is kept by the client until its result arrives, as upstream’s pending request keeps it. Cancel replies are taken by the executor’s spin, as upstream’s executor delivers them: the spin settles the waiting async_cancel_* future and runs its cancel_callback, so a caller that keeps only the callback needs no poll(). A reply no future claims is kept for try_recv_cancel_response, oldest first, up to rclcpp_action::CANCEL_STASH_CAP (4) per client. These need std::promise, so they are hosted-only twice over: a libstdc++ built without threads (arm-none-eabi) refuses them as a freestanding build does.

ServerGoalHandle::execute() and Server<A>::execute(goal_id) move an ACCEPTED goal to EXECUTING and publish the status array; the latter reports NotFound / NotAllowed where upstream raises. create_server executes an ACCEPT_AND_EXECUTE goal before handle_accepted; the goal-id API leaves it ACCEPTED.

Goal slots. Each action sub-client holds at most MAX_CONCURRENT_GOALS (4) requests in flight. A future dropped (or cancel()ed) before its reply frees its request’s slot, unless something else still waits on that reply — a client-wide set_callbacks hook, or an upstream goal (async_send_goal with callbacks, async_get_result). A late reply for a freed slot is dropped. On a freestanding build each of these shapes is a compile error (NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE) naming the goal-id-keyed form above; create_client alone works there.

Timer

Timers fire during spin_once():

void on_timer(void* ctx) {
    // periodic work
}

rclcpp::Timer timer;
RCLCPP_TRY(node.create_wall_timer(timer, 1000, on_timer));     // 1000 ms period
RCLCPP_TRY(node.create_timer_oneshot(timer, 5000, on_timer));  // one-shot after 5 s

timer.cancel();
timer.reset();  // restart from zero

GuardCondition

Guard conditions allow cross-thread signalling:

void on_signal(void* ctx) {
    // handle event
}

rclcpp::GuardCondition guard;
RCLCPP_TRY(node.create_guard_condition(guard, on_signal));

// From another thread:
guard.trigger();
// Callback fires on the next spin_once()

Executor

rclcpp::Executor executor;
RCLCPP_TRY(rclcpp::Executor::create(executor));

rclcpp::Node node;
RCLCPP_TRY(executor.create_node(node, "my_node"));

// Create publishers, subscriptions, etc. on node...

while (executor.ok()) {
    executor.spin_once(10);
}
executor.shutdown();

Spinning

// Global spin (after rclcpp::init_in())
rclcpp::spin_once(10);           // single poll, 10 ms timeout
rclcpp::spin_in(5000);           // spin for 5 seconds
rclcpp::spin_in(5000, 50);       // spin for 5 s, 50 ms poll interval
rclcpp::spin_in();               // until rclcpp::shutdown() flips rclcpp::ok()

// Explicit executor
executor.spin_once(10);
executor.spin_for(5000, 50);    // bounded; executor.spin() blocks until shutdown

How spinning maps onto RTOS tasks and priorities is the subject of Execution Model.

Parameters and /parameter_events

Parameters are methods on the node (declare_parameter<T>, get_parameter<T>, set_parameter<T>, undeclare_parameter), over the executor’s one store. node.start_parameter_services() serves them on the six rcl_interfaces services, so ros2 param reaches them, and publishes /parameter_events (rcl_interfaces/msg/ParameterEvent, rmw_qos_profile_parameter_events) as rclcpp’s node does:

rclcpp::Node node;
RCLCPP_TRY(executor.create_node(node, "ctrl"));
node.declare_parameter<int64_t>("gain", 3);   // before or after: both reported
RCLCPP_TRY(node.start_parameter_services());  // start-up event: new_parameters = [gain]
node.set_parameter<int64_t>("gain", 4);       // changed_parameters = [gain]
node.undeclare_parameter("gain");             // deleted_parameters = [gain]

Each event names the node’s fully qualified name in node; the events are published on the next spin. The image needs the param_services capability. One publisher per executor serves every node. It is on for every served node unless the node was built with rclcpp::NodeOptions().start_parameter_event_publisher(false), which opts that one node out; the other nodes keep publishing.

To react to OTHER nodes’ changes, a hosted build has rclcpp::ParameterEventHandler. It subscribes to /parameter_events once, on the node you give it:

auto handler = std::make_shared<rclcpp::ParameterEventHandler>(node);
auto h = handler->add_parameter_callback(
    "gain", [](const rclcpp::Parameter& p) { use(p.as_int()); }, "/controller");
auto e = handler->add_parameter_event_callback(
    [](const rcl_interfaces::msg::ParameterEvent& ev) { /* every event */ });

Keep the handles: dropping one unregisters it, as upstream’s do. An empty node name means this node, and a relative one resolves against this node’s namespace. A parameter callback sees new and changed values, never deletions. An event callback receives the type it takes, so rcl_interfaces must be generated for the image. The per-parameter handle is rclcpp::ParameterCallbackHandle, as upstream (the handler’s nested ParameterEventHandler::ParameterCallbackHandle names the same type). A freestanding build refuses the handler at compile time; there, subscribe to /parameter_events with create_subscription yourself.

Error handling

All fallible operations return rclcpp::Result. Use RCLCPP_TRY for early return:

rclcpp::Result setup() {
    RCLCPP_TRY(rclcpp::init_in());
    RCLCPP_TRY(rclcpp::create_node(node, "my_node"));
    RCLCPP_TRY(node.create_publisher(pub, "/topic"));
    return rclcpp::Result::success();
}

Check results manually when needed:

rclcpp::Result ret = pub.publish(msg);
if (!ret.ok()) {
    printf("Error: %d\n", ret.raw());
}

ret.raw() is an rclcpp::ErrorCode. The numbering matches rcl_ret_t:

CodeValueMeaning
Ok0success
Error-1generic failure not covered by a more specific code
Timeout-2the deadline elapsed before completion
InvalidArgument-3null pointer, empty topic name, or out-of-range value
NotFound-4entity not found (topic, parameter, service, goal)
AlreadyExists-5duplicate declare or register
Full-6a static pool is exhausted (executor slots, subscription buffers, …)
NotInitialized-7init was never called, or the entity is in its default state
BadSequence-8the call is invalid in the current state
ServiceFailed-9a service request or reply failed
PublishFailed-10publish failed
SubscriptionFailed-11subscription create or take failed
NotAllowed-12not allowed for this entity or backend
Rejected-13the peer considered the request and declined (a rejected goal, a QoS mismatch)
TryAgain-14no data ready yet (non-blocking take); retry later
Reentrant-15a blocking call was made from inside a callback
Unsupported-16not implemented by the active backend
TransportError-100the transport rejected the operation

A Full from a create_* usually means a build-time pool is too small; see Static Pool Inventory for the knob that sizes it.

Two surfaces: freestanding, and NROS_CPP_STD

One set of headers ships two API shapes, and a build picks one.

The freestanding surface is the default and it is not a fallback. It is plain C++14 with no standard library, no exceptions and no RTTI: rclcpp::Node, create_* writing through an out-reference and returning an rclcpp::Result, const char* for names, uint64_t period_ms for durations. It is the surface for every node written for nano-ros, native host builds included.

The std surface is an opt-in and it is reached only by defining NROS_CPP_STD. Defining it turns on the six NROS_CPP_HAS_* capability macros in rclcpp/detail/std_detect.hpp, and with them: the std::string / std::function / std::chrono / std::vector convenience overloads in nros/std_compat.hpp, the RCLCPP_*_STREAM logging macros, and — the piece that actually motivates the flag — rclcpp::Node’s upstream-shaped factories, which return std::shared_ptr and take const std::string& names.

Do I need this flag?

The split is ported rclcpp code versus code written for nano-ros. It is not host versus embedded. A native Linux build does not want the flag any more than a Cortex-M3 build does; a ported node wants it on both.

You are…FlagWhy
writing a new node against rclcpp::Node / rclcpp::Publisher<M> / rclcpp::ExecutornoThe freestanding surface is the whole API you are using. This is the common case on every platform, native included.
compiling an upstream ROS 2 .cpp unmodified — it says class MyNode : public rclcpp::Node, std::make_shared<MyNode>(), create_publisher<M>(...) returning a SharedPtryesThose signatures are spelled in std::shared_ptr and std::string. Without the flag they do not exist and you get a compile error naming a missing overload.
passing a std::string topic name or a std::chrono period into an otherwise nano-ros-native fileyesThose are the std_compat.hpp overloads.
building for Zephyr, FreeRTOS, NuttX, ThreadX, or any -ffreestanding toolchainnoAnd here it is not merely unnecessary: asking for the std surface asks the headers to #include <string>, which on those toolchains is either absent or a hard #error.

If you are unsure, build without it. The failure direction is loud: a missing overload at compile time, never a silent behaviour change.

Turning it on

Define it on the target, not per file:

target_compile_definitions(my_ported_node PRIVATE NROS_CPP_STD=1)

Per target rather than per #define-before-include, because the macro changes the layout of rclcpp::Node (it carries hosted-only members and an enable_shared_from_this base), so two translation units of one image that disagree about it are an ODR/layout break rather than a missing function. An image may set the flag on one module and not another, so keep the definition at target granularity and give every translation unit of a target the same answer.

A ported package usually needs none of this. nano-ros’s ament surface sets NROS_CPP_STD=1 on every target its ament_* verbs create, and find_package(rclcpp) sets it on whatever links rclcpp::rclcpp, so a ported package already has the surface it needs; see Porting a ROS 2 C++ node.

The ported shape

With the flag set, an upstream node’s own spelling compiles: std::make_shared, SharedPtr returns, a std::string topic, a QoS depth, a capturing lambda and a std::chrono period.

#include <rclcpp/rclcpp.hpp>
#include "std_msgs/std_msgs.hpp"

using namespace std::chrono_literals;

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<rclcpp::Node>("talker");
    auto pub = node->create_publisher<std_msgs::msg::Int32>("chatter", 10);
    int count = 0;
    auto timer = node->create_wall_timer(1s, [&]() {
        std_msgs::msg::Int32 msg;
        msg.data = ++count;
        pub->publish(msg);
    });
    rclcpp::spin(node);
    rclcpp::shutdown();
}

A failed create_* here ABORTS, naming the call, because upstream throws and nano-ros has no exceptions. To handle the failure instead, use the Result-returning forms above.

Why it is not detected for you

Earlier releases discovered the capability macros from the include path with __has_include, so a hosted compiler handed you the std-flavoured API whether you asked for it or not. That was removed because no probe answers the question correctly on both embedded toolchains:

__STDC_HOSTED____has_include(<string>)#include <string>
arm-none-eabi 13.2, -ffreestanding (FreeRTOS)0TRUEhard #error
Zephyr -nostdinc++, minimal libcpp1FALSEabsent
hosted g++ 12.31TRUEworks

Each probe is right on exactly one embedded toolchain and wrong on the other: libstdc++ 13 added bits/requires_hosted.h, so header presence stopped implying usability. Only the request is right on both, because it is not a probe — it is you saying which surface you want. The full measurement is in the comments of rclcpp/detail/std_detect.hpp.

Zephyr

A Zephyr application reaches the C++ API through the nano-ros Zephyr module. The module, the image’s system.toml and the prj.conf split are covered in Zephyr integration; the RMW, locator and domain come from the image, never from CONFIG_NROS_* lines in prj.conf. The C++ side needs CONFIG_CPP=y and CONFIG_STD_CPP14=y in prj.conf (Zephyr’s own settings), and the application’s CMakeLists.txt generates the message bindings:

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_app LANGUAGES CXX)

nros_generate_interfaces(std_msgs LANGUAGE CPP)
target_sources(app PRIVATE src/main.cpp)

A minimal main() waits for the network, opens the session with the locator and domain the image rendered into Kconfig, and spins:

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

#include <nros/platform_zephyr.h>

#include <rclcpp/rclcpp.hpp>
#include "std_msgs.hpp"

LOG_MODULE_REGISTER(my_app, LOG_LEVEL_INF);

int main(void)
{
    nros_platform_zephyr_wait_network(CONFIG_NROS_INIT_DELAY_MS);

    rclcpp::Result ret = rclcpp::init_in(CONFIG_NROS_ZENOH_LOCATOR, CONFIG_NROS_DOMAIN_ID);
    if (!ret.ok()) return 1;

    rclcpp::Node node;
    if (!rclcpp::create_node(node, "my_node").ok()) return 1;

    // ... create publishers, subscriptions, etc.

    while (true) {
        rclcpp::spin_once(100);
    }
}

nros_platform_zephyr_wait_network (<nros/platform_zephyr.h>) is defined in every Zephyr image. On an image with no IP stack (CONFIG_NET_SOCKETS off, for example a serial-only zenoh image) it returns 0 at once, so the call needs no guard. CONFIG_NROS_INIT_DELAY_MS is the module’s start-up delay; your main() is its only reader.

Building the API docs from source

The Doxygen site linked above is the published copy. Building it from the nano-ros source tree: see Internals → Build Commands.

Design background