Question
std::unique_ptr<T> cannot be copied, but it can be moved. Why does the following function compile when it returns the named local variable p without calling std::move explicitly?
#include <iostream>
#include <memory>
std::unique_ptr<int> foo()
{
std::unique_ptr<int> p(new int(10));
return p; // Why is this valid?
// return std::move(p); // This also works.
}
int main()
{
std::unique_ptr<int> p = foo();
std::cout << *p << '\n';
}
If std::unique_ptr has no copy constructor, why does return p; not try to copy it and cause a compiler error? Is this behavior a special compiler case, or is it defined by the C++ language rules?
Short Answer
C++ has a language rule that allows an eligible local object returned by value to be treated as movable when a copy would otherwise be needed. Therefore, return p; can use std::unique_ptr's move constructor. In many builds, named return value optimization (NRVO) eliminates even that move by constructing p directly in the caller's result storage.
Concept
std::unique_ptr represents exclusive ownership: exactly one unique_ptr owns a resource at a time. Copying it would create two owners, so its copy constructor is deleted.
std::unique_ptr<int> a = std::make_unique<int>(10);
// std::unique_ptr<int> b = a; // Error: copying is not allowed
Moving is different. A move transfers ownership from one object to another. After a move, the source unique_ptr is valid but normally empty.
std::unique_ptr<int> a = std::make_unique<int>(10);
std::unique_ptr<int> b = std::move(a);
// a.get() is normally nullptr
// b owns the int
A return statement has a useful special language rule: when returning an eligible non-volatile local object by value, C++ can first try to treat that named object as an rvalue for overload resolution. This is often called an implicit move from a return statement.
So this code:
return p;
can select the move constructor conceptually similar to:
return std::move(p);
It is not a unique_ptr-specific exception and not merely a compiler extension. It is a C++ language rule designed to make returning move-only values natural and safe.
There is also an optimization that may occur first: named return value optimization (NRVO). With NRVO, the compiler constructs the function's result directly in the caller's destination. No copy and no move constructor need to run.
Two important details:
return p;keeps NRVO possible and permits an implicit move if NRVO is not performed.return std::move(p);usually prevents NRVO, forcing a move instead. For a local variable being returned, preferreturn p;.
Mental Model
Think of a std::unique_ptr as a key to a locked storage unit. Only one person may hold the key.
- Copying would mean duplicating the key, which is forbidden.
- Moving means handing the one key to someone else.
- Returning a local
unique_ptrmeans the function is handing its key to the caller as it leaves.
C++ recognizes this common handoff. When it sees return p; for an eligible local object, it knows p is about to leave its scope and is allowed to use the handoff operation (move) rather than attempting to duplicate it (copy).
Sometimes the key is placed directly in the caller's hand from the beginning. That is NRVO: there is no handoff step at all.
Syntax and Examples
Use a value return type and return the local owning object by name.
#include <memory>
std::unique_ptr<int> createNumber()
{
auto number = std::make_unique<int>(42);
return number;
}
int main()
{
std::unique_ptr<int> result = createNumber();
}
number is a local variable with automatic storage duration. It is an eligible return object, so C++ can move from it if necessary.
A more direct version is also common:
#include <memory>
std::unique_ptr<int> createNumber()
{
return std::make_unique<int>(42);
}
This returns a temporary result object. Modern C++ can construct it directly as the function result.
Preferred and discouraged forms
std::unique_ptr<int> preferred()
{
auto p = std::make_unique<int>(10);
return p; // Good: NRVO remains possible; otherwise implicit move.
}
std::unique_ptr<int> alsoGood()
{
return std::make_unique<int>(10); // Good: direct result construction is possible.
}
std::unique_ptr<int> usuallyUnnecessary()
{
auto p = std::make_unique<int>(10);
return std::move(p); // Valid, but may disable NRVO.
}
Use std::move when moving from an object that will continue to exist after the current expression. Do not add it automatically to a local variable that is simply being returned.
Step by Step Execution
Consider this function:
std::unique_ptr<int> makeValue()
{
auto p = std::make_unique<int>(10);
return p;
}
A conceptual execution without NRVO looks like this:
pis created and becomes the sole owner of dynamically allocatedint(10).- The return statement needs to initialize the function's return object.
- Because
pis an eligible local return object, the language treats it as movable when selecting a constructor. std::unique_ptr's move constructor is selected. Ownership of theintis transferred to the return object.pbecomes empty.pis destroyed as the function exits. Since it is empty, it deletes nothing.- The caller initializes its
unique_ptrfrom the returned value. Depending on the C++ version and optimization, this step may also be elided or use a move.
With NRVO, the conceptual steps are shorter:
- Storage for the caller's result is used for the local result object.
- The
unique_ptris constructed directly there. - No separate return object and no move are needed.
Both cases preserve unique ownership: there is never a copy that creates two owning unique_ptr objects.
Real World Use Cases
Returning move-only objects by value is a normal C++ API design pattern.
-
Factories: Create an object only when construction succeeds.
std::unique_ptr<Connection> connectToServer(const Config& config) { return std::make_unique<Connection>(config.host, config.port); } -
Resource acquisition: Return an object that owns a file, socket, lock, or operating-system handle.
std::unique_ptr<File> openFile(const std::string& path) { auto file = std::make_unique<File>(path); return file; } -
Polymorphic objects: Return a base-class
unique_ptrwhile selecting a concrete implementation.std::unique_ptr<Parser> makeParser(bool useJson) { if (useJson) { return std::make_unique<JsonParser>(); } return std::make_unique<TextParser>(); } -
Building containers or helper objects: Return a completed object without requiring heap allocation by the caller. This applies to many movable types, not only
unique_ptr, such asstd::vector,std::string, and file-wrapper classes.
Real Codebase Usage
In real projects, functions commonly return ownership rather than writing into an output parameter.
Return ownership explicitly
std::unique_ptr<Widget> createWidget(const WidgetConfig& config)
{
auto widget = std::make_unique<Widget>(config);
widget->initialize();
return widget;
}
The return type documents ownership: the caller receives exclusive responsibility for the Widget.
Use guard clauses with direct returns
std::unique_ptr<User> findUser(const Database& db, int id)
{
if (id <= 0) {
return nullptr;
}
auto record = db.lookup(id);
if (!record) {
return nullptr;
}
return std::make_unique<User>(*record);
}
Returning nullptr is valid because std::unique_ptr can represent no owned object.
Transfer ownership deliberately between layers
void registerPlugin(std::unique_ptr<Plugin> plugin);
void loadPlugin()
{
auto plugin = std::make_unique<Plugin>();
registerPlugin(std::move(plugin));
}
Here std::move is needed because plugin is being passed to another function, not returned using the return-statement rule. After the call, plugin should be treated as empty.
Prefer std::make_unique
return std::make_unique<Service>(settings);
std::make_unique is clearer and safer than manually writing new in most code.
Common Mistakes
Trying to copy a unique_ptr
auto first = std::make_unique<int>(10);
auto second = first; // Error: copy constructor is deleted
Use a move only when ownership should leave first:
auto second = std::move(first);
Adding std::move to every return
std::unique_ptr<int> makeNumber()
{
auto p = std::make_unique<int>(10);
return std::move(p); // Valid, but usually not preferred
}
This may block NRVO. Write return p; for an eligible local variable. The compiler can still move when it cannot elide the return.
Returning a reference to a local object
std::unique_ptr<int>& broken()
{
auto p = std::make_unique<int>(10);
return p; // Error or undefined design: p is destroyed on return
}
A local variable's lifetime ends when the function returns. Return by value instead.
Using a moved-from unique_ptr as though it still owns data
auto p = std::make_unique<int>(10);
auto owner = std::move(p);
// std::cout << *p; // Wrong: p is normally null after the move
A moved-from unique_ptr is valid and can be assigned or destroyed, but do not dereference it unless you assign a new resource first.
Assuming a move constructor always runs
Do not use move-constructor side effects for program logic. NRVO and other copy-elision rules can mean that no move occurs at runtime.
Comparisons
| Situation | Typical code | What happens |
|---|---|---|
| Return an eligible local by value | return p; | NRVO may occur; otherwise C++ can implicitly move p. |
| Return a freshly created value | return std::make_unique<T>(); | The result can often be constructed directly in return storage. |
| Return an explicitly moved local | return std::move(p); | Valid move, but can prevent NRVO for a named local. |
| Pass ownership to another function | take(std::move(p)); | Explicit move is required because this is not the special return context. |
| Copy ownership | auto q = p; | Error for std::unique_ptr; copying would violate exclusive ownership. |
std::unique_ptr versus std::shared_ptr
| Type | Ownership model | Copyable? | Movable? |
|---|---|---|---|
std::unique_ptr<T> | One owner | No | Yes |
std::shared_ptr<T> | Multiple shared owners | Yes | Yes |
Do not choose shared_ptr merely to avoid thinking about moves. Use unique_ptr when one clear owner is correct; it expresses intent and avoids reference-counting overhead.
Cheat Sheet
-
std::unique_ptris move-only: it cannot be copied. -
Return a local
unique_ptrwithreturn local;. -
In a return statement, an eligible local object can be moved implicitly.
-
The behavior is defined by C++ language rules; it is not unique to
std::unique_ptr. -
NRVO may remove the move entirely.
-
Prefer:
auto p = std::make_unique<T>(); return p; -
Also prefer direct construction when possible:
return std::make_unique<T>(); -
Usually avoid this for a local return value:
return std::move(p);It is valid but can inhibit NRVO.
-
Use
std::movewhen transferring a namedunique_ptrto another variable, function, or container. -
After
std::move(p), do not dereferencepunless it has been assigned a new value.
FAQ
Does return p; copy a std::unique_ptr?
No. For an eligible local object returned by value, C++ can treat p as movable. If NRVO is not used, std::unique_ptr's move constructor is selected.
Is return p; exactly the same as return std::move(p);?
Not quite. Both can transfer ownership, but return p; allows named return value optimization. return std::move(p); can prevent that optimization.
Is this behavior specific to std::unique_ptr?
No. The implicit-move return rule applies to suitable local objects of other movable types too, such as std::vector and std::string.
Why is moving safe when the local variable is about to be destroyed?
The move transfers the owned pointer out of the local object. The local unique_ptr is then empty, so destroying it does not delete the resource. The returned unique_ptr is the new sole owner.
Does a move constructor always execute when I return a local object?
No. NRVO may construct the object directly in the caller's result location, so neither a copy nor a move constructor runs.
Should I always use std::move when returning an object?
No. For a local variable of the function's return type, use return local;. Add std::move only when it is genuinely necessary, such as transferring ownership to a different function.
Can I return nullptr from a function that returns std::unique_ptr<T>?
Yes.
std::unique_ptr<int> findValue(bool found)
{
if (!found) {
return nullptr;
}
return std::make_unique<int>(10);
}
Mini Project
Description
Build a small factory that creates different document types and returns them through std::unique_ptr. The project demonstrates exclusive ownership, polymorphism, direct returns, and returning a named local object without writing std::move.
Goal
Create a document factory that returns a std::unique_ptr<Document> and lets the caller use the created document safely.
Requirements
Return a std::unique_ptr<Document> from a factory function.
Support at least two document formats.
Return nullptr for an unsupported format.
Use std::make_unique to create concrete document objects.
Return one named local std::unique_ptr using return local; without std::move.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.