C++ / a working model

36 / 163   ·   C++14   ·   9 min

Parameter passing and ownership contracts

Keep this sentence

When choosing a parameter type, first decide whether the function only reads, needs to modify, or takes ownership, then compare copy cost. Pass small values by value, large read-only objects by const reference, nullable borrows by pointer, and exclusive ownership by passing unique_ptr by value. These types communicate different contracts.

In this lesson
  1. Pass-by-value is not uniformly inefficient
  2. Distinguish borrow and takeover in the signature
  3. Choose an owning representation when retaining data
  4. Example
  5. Exercise

Pass-by-value is not uniformly inefficient

A by-value parameter is an independent parameter object. Modifying it usually does not modify the caller's object. Integers and lightweight handles are simple and clear when passed by value; whether that is faster depends on the type and the target platform. Passing a large read-only object by const reference can avoid a copy, but it introduces an aliasing relationship and requires the target to remain valid for the duration of the call.

Copying a pointer copies only the pointer value. The function can modify the same target through that copy, yet it cannot replace the caller's pointer by assigning a new address to the copy. If you truly need to reset an external pointer, you can pass a reference to a pointer. More often, returning a new result makes the interface easier to understand.

Distinguish borrow and takeover in the signature

T& suits a required mutable borrow, const T& suits a required read-only borrow, and T* can express an optional target. None of them should be assumed to transfer ownership. If a function needs to destroy or keep an exclusive resource for a long time, it should take std::unique_ptr<T> by value so the caller uses move to hand over responsibility explicitly.

A function that only reads T has no need to take a shared_ptr. Leave smart-pointer parameters for interfaces that must manipulate ownership. That avoids unnecessary reference-count updates and lets callers who use automatic objects or unique_ptr avoid being forced into a particular storage strategy.

Choose an owning representation when retaining data

In the example, decorate takes a string by value because it needs a local mutable copy and returns the result by value. The caller's original string is unchanged. consume takes a unique_ptr and, after returning an integer, the parameter destructor reclaims the resource. The assertions around the call make the exclusive-ownership transfer explicit.

When text must be stored inside an object, taking a string by value and then moving it into a member is a common pattern. An lvalue argument is copied; a temporary can be moved. Passing string_view and span by value copies only the view, not the underlying data. They must not be stored directly into an asynchronous task that outlives the input.

Pitfalls

  • std::move is only a conversion that enables a move operation; it does not guarantee that every type actually moves, and it does not by itself release a resource.
  • A const-reference parameter does not automatically make a reference stored by the function valid long-term; continued borrowing after the call needs a separate lifetime guarantee.

Run an example

Minimum C++14 · complete program · Download .cpp

#include <cassert>
#include <memory>
#include <string>
#include <utility>

std::string decorate(std::string text) {
    text += '!';
    return text;
}

int consume(std::unique_ptr<int> value) {
    assert(value);
    return *value;
}

int main() {
    std::string original = "C++";
    assert(decorate(original) == "C++!");
    assert(original == "C++");
    auto owner = std::make_unique<int>(42);
    int result = consume(std::move(owner));
    assert(result == 42);
    assert(!owner);
}

Compile locally

g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread memory-parameters.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Design a function parameter that only reads a Record during the call and allows there to be no Record. Does it need a shared_ptr?

Show a reference answer

Declare it as void inspect(const Record* record), check whether record is null inside the function, then read-only access. A shared_ptr is not needed because nothing is stored or shared. If the target is required, use const Record&. If it must be queued for a later task, switch to an owned value copy, or take shared_ptr by value explicitly to extend lifetime.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog