36 / 163 · C++14 · 9 min
Parameter passing and ownership contracts
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
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::moveis 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 && ./exampleExpected 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.