Modern C++ for robotics code
C++ lets you decide exactly when a resource is acquired and released. The point of this module is to use that control so you never have to think about it again: every C object owned by exactly one C++ object, freed exactly once, on every way out of a function, including the ones you did not plan for.
5 code rules · 13/13 checks pass · 11 sources · fact-checked and reviewed
Key ideas
A raw pointer says where, not who
mj_loadXMLhands back an address. Nothing in that address records who must callmj_deleteModel, or when. Every early return and every exception is a path on which the call can be forgotten, or made twice.[1]Tie the release to an object's lifetime
RAII (resource acquisition is initialisation): acquire in a constructor, release in the destructor. Destructors run whenever a scope is left, normally or by an exception that is caught somewhere, so the release is not skipped. The exceptions are deliberate exits:
std::exitandstd::abortdo not unwind the stack.[1][2][3][4]Own it in the statement that acquires it
Wrap a raw pointer in its owner on the line that creates it (F3.03). If anything after that throws, the owners already built are destroyed on the way out; a raw pointer held at that moment is simply abandoned. In the check project the owned version leaks nothing when its second step fails, and the raw version leaks the first object.[1][8]
The rule of zero
If every member already manages itself, write none of the destructor, copy and move functions, and the compiler generates correct ones (F3.04). With
unique_ptrmembers the class comes out move-only, with moves that cannot throw. If you do write or delete any of the five, write or delete all of them.[2][7]Moving hands ownership over
std::movedoes not move anything by itself. It marks an object as safe to move from, and the move happens when another owner takes it. A moved-fromunique_ptris guaranteed to be empty (F3.05); most other moved-from standard types are only "valid but unspecified".[5][9][10]
Rules
The same entries as the formula sheet. Open an entry's details for its symbols and the checks behind it.
Own a C object with unique_ptr and its delete function
The deleter runs once per object owned: when the owner is destroyed, reset or move-assigned over, and never on a null pointer or after release(). The owner cannot be copied.
using ModelPtr = std::unique_ptr<mjModel, decltype(&mj_deleteModel)>; ModelPtr model(mj_loadXML(path, nullptr, err, n), &mj_deleteModel); if (!model) throw std::runtime_error(err);Symbols and checks
- ✓ ownership checks ran clean under ASan + UBSan (exit code)expected 0 · got 0 · absolute error 0its static_asserts (move-only, noexcept moves) compiled
- ✓ objects still alive after the owner's scope endsexpected 0 · got 0 · absolute error 0
- ✓ deleter calls with a null pointer after a failed loadexpected 0 · got 0 · absolute error 0
Members are destroyed in reverse order of declaration
Always, whatever order the constructor's initializer list is written in. Bases and members are destroyed in the reverse order of their construction.
ModelPtr model_; // declared first: built first, destroyed last DataPtr data_; // depends on model_: declared second, destroyed firstSymbols and checks
- ✓ -Wall warns about an out-of-order initializer list (clang++)observed as statedUbuntu clang version 18.1.3 (1ubuntu1)
- ✓ list written b, a: built a, b; destroyed b, a (clang++)observed as statedUbuntu clang version 18.1.3 (1ubuntu1)
- ✓ -Wall warns about an out-of-order initializer list (g++)observed as statedg++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
- ✓ list written b, a: built a, b; destroyed b, a (g++)observed as statedg++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
- ✓ members destroyed in reverse orderobserved as stated
Take ownership in the statement that acquires
Any exit, including an exception, destroys the owners already built. A raw pointer held at that moment is simply abandoned.
ModelPtr model(mj_loadXML(...), &mj_deleteModel); // owned at once DataPtr data(mj_makeData(model.get()), &mj_deleteData); if (!data) throw std::runtime_error("mj_makeData failed"); // model is still freedSymbols and checks
- ✓ owned: objects leaked when the second step failsexpected 0 · got 0 · absolute error 0
- ✓ raw pointers: objects leaked when the second step failsexpected 1 · got 1 · absolute error 0
The rule of zero
When every member manages itself, write none of the five special members. If you write or delete any one of them, write or delete all five (the rule of five).
class Model { ModelPtr model_; DataPtr data_; // no destructor, copy or move written: the members do it all }; static_assert(!std::is_copy_constructible_v<Model>); static_assert(std::is_nothrow_move_constructible_v<Model>);Symbols and checks
- ✓ ownership checks ran clean under ASan + UBSan (exit code)expected 0 · got 0 · absolute error 0its static_asserts (move-only, noexcept moves) compiled
- ✓ after a move, each C object is freed exactly onceobserved as stated
A moved-from unique_ptr is empty
Guaranteed for unique_ptr. Unless the standard says otherwise, a moved-from standard library object is only in a "valid but unspecified" state, so do not read it before assigning to it.
auto b = std::move(a); // b owns the object now assert(a == nullptr); // guaranteed by the standard for unique_ptrSymbols and checks
- ✓ a moved-from unique_ptr is nullobserved as stated
- ✓ the new owner holds the objectobserved as stated
Worked example: arm::sim::Model, line by line
The class that owns MuJoCo's model and simulation state in the arm repository: two pointers, no destructor, and every path out of load() safe. The check project behind this page builds the same pattern around a stand-in C library with counters on every create and destroy, and runs it under AddressSanitizer and UBSan.
Two owning pointer types
decltype(&mj_deleteModel)is the type of a pointer to MuJoCo's delete function, so eachunique_ptrstores the function it will call.using ModelPtr = std::unique_ptr<mjModel, decltype(&mj_deleteModel)>; using DataPtr = std::unique_ptr<mjData, decltype(&mj_deleteData)>;Members, in dependency order
mjDatabelongs to anmjModel, so the model is declared first and destroyed last. The constructor is private: the only way to get aModelisload(), so everyModelstarts out holding a model that loaded. (One that has been moved from is empty, like theunique_ptrs inside it.)private: Model(ModelPtr model, DataPtr data) noexcept; ModelPtr model_; // destroyed second DataPtr data_; // destroyed firstTake ownership at once
Lightly condensed from
model.cpp. Each pointer is wrapped on the line that creates it. Ifmj_makeDatafails, the throw leavesload()andmodelfrees the model on the way out. MuJoCo's error message travels in the exception.std::array<char, 1000> error{}; ModelPtr model(mj_loadXML(path.string().c_str(), nullptr, error.data(), static_cast<int>(error.size())), &mj_deleteModel); if (!model) throw std::runtime_error("failed to load " + path.string() + ": " + error.data()); DataPtr data(mj_makeData(model.get()), &mj_deleteData); if (!data) throw std::runtime_error("mj_makeData failed");Hand both over
std::movelets the private constructor take ownership;modelanddataare left empty, so nothing is freed twice.return Model(std::move(model), std::move(data));What was not written
No destructor, no copy or move functions: the members do all of it. These assertions compile in the check project, so the claims are enforced by the compiler, not by a comment.
static_assert(!std::is_copy_constructible_v<Model>); static_assert(std::is_nothrow_move_constructible_v<Model>); static_assert(std::is_nothrow_move_assignable_v<Model>);What the check project observed
The same pattern around a stand-in C library, counting every create and destroy, under AddressSanitizer and UBSan.[8]
Owned with unique_ptr versus raw pointers Situation Owned Raw pointers Objects alive after the owner goes out of scope none not applicable Objects leaked when the second step fails none one Order of destruction data, then model by hand Deleter called on a null pointer after a failed load never not applicable Frees after moving the owner one each not applicable
Put the owning in the members and the class needs no clean-up code at all. Every path out of every function is then safe, including the paths you did not think of when you wrote it.
Best practice
Wrap a raw pointer in its owner on the line that creates it.
Nothing can run between acquiring and owning, so nothing can leak in between.[1]
Never call a delete function yourself outside a deleter.
A manual call is one more place the release can be skipped or repeated. Let the owner make the call, once.[1]
Declare members in dependency order, and say so in a comment.
The language fixes destruction order by declaration order. A reader who reorders members to "tidy up" needs to know the order matters.[6][7]
Prefer unique_ptr; reach for shared_ptr only for genuinely shared ownership.
unique_ptr is simpler, you know exactly when destruction happens, and it maintains no use count.[1]
Keep moves noexcept.
The standard library uses a move that cannot throw more efficiently, for example when a vector grows. With unique_ptr members, the rule of zero gives you noexcept moves for free.[7]
Make construction all-or-nothing.
A private constructor plus a factory that returns a complete object or throws means no half-built object is ever handed out. The only empty
Modelis one that has been moved from, which you should not use.
Questions
- Why not shared_ptr?
- Nothing shares a model: one
Modelowns it. shared_ptr would add a use count, make the moment of destruction less predictable, and allow copies that hide who is responsible.[1] - Do destructors run if an exception is never caught?
- Not necessarily. If no handler is found,
std::terminateis called, and whether the stack is unwound first is up to the implementation. Catch exceptions inmain()if you rely on clean-up at exit, such as flushing a log.[3] - Do I need to check for a null pointer before the deleter runs?
- No. A
unique_ptrnever calls its deleter on a null pointer; an empty owner simply does nothing when destroyed. The check project counts the calls after a failed load: zero.[5][8] - What is left in
aafterauto b = std::move(a);? - For a
unique_ptr, nothing:ais guaranteed to be null andbowns the object. For most other standard types,ais valid but unspecified, so assign to it before you read it.[5][9] - Does the order in my constructor's initializer list matter?
- Not for the order of construction, which always follows the declaration order, but writing it in a different order misleads the reader, and an initializer that reads a later member reads it before it is set. GCC and Clang both warn under
-Wall(-Wreorder; Clang reports it as-Wreorder-ctor); the check project compiles an out-of-order list with each, and runs it to confirm the order.[6][7][8] - Is this the pattern for a file handle, a socket or a lock too?
- Yes: any acquire and release pair. Writing one yourself, from memory, is the best test that this page has stuck.[1]
Watch
- listed
Back to Basics: Move Semantics (part 1 of 2) (opens in a new tab)[10]
Klaus Iglberger · CppCon 2019, CppCon YouTube channel
What std::move really does, moved-from objects, and the rules of zero and five, built up from first principles.
- listed
Back to Basics: Move Semantics (part 2 of 2) (opens in a new tab)[11]
Klaus Iglberger · CppCon 2019, CppCon YouTube channel
The second half: forwarding references and perfect forwarding. Watch it once part one has settled.
Read
- read
Back to Basics: RAII and the Rule of Zero (slides) (opens in a new tab)[2]
Arthur O'Dwyer · CppCon 2019; official slides in the CppCon/CppCon2019 repository
RAII, exception safety and the rule of zero, as clear slides. The talk itself is on the CppCon YouTube channel.
- read
C++ Core Guidelines: R.1, R.20, R.21, E.6 (opens in a new tab)[1]
Bjarne Stroustrup, Herb Sutter (editors) · Standard C++ Foundation
The guidelines on resource management; R.1 is the whole idea in one rule.
- read
C++ Core Guidelines: C.20, C.21, C.47, C.66 (opens in a new tab)[7]
Bjarne Stroustrup, Herb Sutter (editors) · Standard C++ Foundation
The rules of zero and five, as the guidelines put them.
Sources
Numbered in order of first use on this page. read: checked against the claim. listed: exists and covers the topic, wording not checked. derived: worked out here and checked numerically.
- [1]read
C++ Core Guidelines: R.1, R.20, R.21, E.6 (opens in a new tab)
Bjarne Stroustrup, Herb Sutter (editors) · Standard C++ Foundation
R.1 (RAII), R.20 (smart pointers for ownership), R.21 (prefer unique_ptr to shared_ptr), E.6 (RAII to prevent leaks)
Wrap every acquire/release pair in an object that acquires in its constructor and releases in its destructor; express ownership with smart pointers; unique_ptr is simpler, more predictable and faster than shared_ptr, which maintains a use count. Read from the guidelines repository.
- [2]read
Back to Basics: RAII and the Rule of Zero (slides) (opens in a new tab)
Arthur O'Dwyer · CppCon 2019; official slides in the CppCon/CppCon2019 repository
Slides 38–46 (RAII and exception safety; the Rule of Zero)
RAII and exception safety; classes that only use library components should write no special member functions (the Rule of Zero). The talk is also on the CppCon YouTube channel.
- [3]read
ISO/IEC JTC1/SC22/WG21, April 2020 (the C++20 working draft)
[except.ctor] (stack unwinding); [except.handle] (no handler found)
As control passes from a throw to a handler, automatic objects are destroyed (stack unwinding). If no matching handler is found, std::terminate is called, and whether the stack is unwound first is implementation-defined.
- [4]read
ISO/IEC JTC1/SC22/WG21, April 2020 (the C++20 working draft)
[support.start.term] (exit, abort)
Objects with automatic storage duration are not destroyed as a result of calling exit(); abort() terminates the program without executing destructors.
- [5]read
Working Draft, Standard for Programming Language C++ (N4861), General utilities (opens in a new tab)
ISO/IEC JTC1/SC22/WG21, April 2020 (the C++20 working draft)
[unique.ptr.single.dtor]; [unique.ptr.single.ctor] (move constructor postconditions)
unique_ptr's destructor does nothing if get() == nullptr and otherwise calls get_deleter()(get()); after a move construction, u.get() == nullptr.
- [6]read
Working Draft, Standard for Programming Language C++ (N4861), Classes (opens in a new tab)
ISO/IEC JTC1/SC22/WG21, April 2020 (the C++20 working draft)
[class.dtor] (members destroyed in reverse order of construction); [class.base.init] (members initialized in declaration order)
Non-static data members are initialized in the order they are declared, whatever the initializer list says, and destroyed in the reverse order.
- [7]read
C++ Core Guidelines: C.20, C.21, C.47, C.66 (opens in a new tab)
Bjarne Stroustrup, Herb Sutter (editors) · Standard C++ Foundation
C.20 (rule of zero), C.21 (rule of five), C.47 (member declaration order), C.66 (noexcept moves)
Avoid defining default operations when you can; if you define or delete any copy, move or destructor function, define or delete them all; members are initialized in declaration order. Read from the guidelines repository (August 2026).
- [8]derived
Compiled checks for modules 02 to 04 (tools/robotics/cpp)
A small CMake project that mirrors the arm repository, built with presets and run by verify.py; every C++, CMake and GoogleTest claim marked checked is observed there.
- [9]read
ISO/IEC JTC1/SC22/WG21, April 2020 (the C++20 working draft)
[lib.types.movedfrom]
Unless otherwise specified, moved-from standard library objects are placed in a valid but unspecified state.
- [10]listed
Back to Basics: Move Semantics (part 1 of 2) (opens in a new tab)
Klaus Iglberger · CppCon 2019, CppCon YouTube channel
std::move, rvalue references, moved-from objects, noexcept move operations and the rules of zero and five. Topics confirmed from the official slides (CppCon/CppCon2019 repository); video id from a listing of the CppCon channel.
- [11]listed
Back to Basics: Move Semantics (part 2 of 2) (opens in a new tab)
Klaus Iglberger · CppCon 2019, CppCon YouTube channel
Forwarding references and perfect forwarding. Topics confirmed from the official slides; video id from a listing of the CppCon channel.