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

  1. A raw pointer says where, not who

    mj_loadXML hands back an address. Nothing in that address records who must call mj_deleteModel, or when. Every early return and every exception is a path on which the call can be forgotten, or made twice.[1]

  2. 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::exit and std::abort do not unwind the stack.[1][2][3][4]

  3. unique_ptr makes any C API an RAII type

    std::unique_ptr<T, D> calls its deleter once, when it is destroyed, unless it is empty (F3.01). Give it MuJoCo's own delete function and it owns an mjModel properly. It cannot be copied, so there is never a second owner to free the object again.[1][5]

  4. Declaration order is destruction order, reversed

    Members are built in the order they are declared and destroyed in the reverse order (F3.02). Declare what the others depend on first, and it is freed last. The order the constructor lists them in changes nothing.[6][7]

  5. 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]

  6. 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_ptr members 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]

  7. Moving hands ownership over

    std::move does 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-from unique_ptr is 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.

  1. 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);
    F3.01✓ 3/3 checked[1][5]
    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
  2. 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 first
    F3.02✓ 5/5 checked[6][7]
    Symbols 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
  3. 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 freed
    F3.03✓ 2/2 checked[1][2]
    Symbols 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
  4. 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>);
    F3.04✓ 2/2 checked[2][7]
    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
  5. 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_ptr
    F3.05✓ 2/2 checked[5][9][10]
    Symbols 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.

  1. Two owning pointer types

    decltype(&mj_deleteModel) is the type of a pointer to MuJoCo's delete function, so each unique_ptr stores the function it will call.

    using ModelPtr = std::unique_ptr<mjModel, decltype(&mj_deleteModel)>;
    using DataPtr  = std::unique_ptr<mjData,  decltype(&mj_deleteData)>;
  2. Members, in dependency order

    mjData belongs to an mjModel, so the model is declared first and destroyed last. The constructor is private: the only way to get a Model is load(), so every Model starts out holding a model that loaded. (One that has been moved from is empty, like the unique_ptrs inside it.)

    private:
      Model(ModelPtr model, DataPtr data) noexcept;
      ModelPtr model_;  // destroyed second
      DataPtr  data_;   // destroyed first
  3. Take ownership at once

    Lightly condensed from model.cpp. Each pointer is wrapped on the line that creates it. If mj_makeData fails, the throw leaves load() and model frees 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");
  4. Hand both over

    std::move lets the private constructor take ownership; model and data are left empty, so nothing is freed twice.

    return Model(std::move(model), std::move(data));
  5. 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>);
  6. 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
    SituationOwnedRaw pointers
    Objects alive after the owner goes out of scopenonenot applicable
    Objects leaked when the second step failsnoneone
    Order of destructiondata, then modelby hand
    Deleter called on a null pointer after a failed loadnevernot applicable
    Frees after moving the ownerone eachnot 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

Questions

Why not shared_ptr?
Nothing shares a model: one Model owns 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::terminate is called, and whether the stack is unwound first is up to the implementation. Catch exceptions in main() 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_ptr never 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 a after auto b = std::move(a);?
For a unique_ptr, nothing: a is guaranteed to be null and b owns the object. For most other standard types, a is 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

Read

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. [1]

    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.

    read
  2. [2]

    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.

    read
  3. [3]

    Working Draft, Standard for Programming Language C++ (N4861), Exception handling (opens in a new tab)

    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.

    read
  4. [4]

    Working Draft, Standard for Programming Language C++ (N4861), Language support library (opens in a new tab)

    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.

    read
  5. [5]

    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.

    read
  6. [6]

    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.

    read
  7. [7]

    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).

    read
  8. [8]

    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.

    derived
  9. [9]

    Working Draft, Standard for Programming Language C++ (N4861), Library introduction (opens in a new tab)

    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.

    read
  10. [10]

    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.

    listed
  11. [11]

    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.

    listed