Question

Returning std::unique_ptr Without std::move in C++

cppc++11unique-ptrmove-semantics

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, prefer return 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_ptr means 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.

Take Quiz

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:

  1. p is created and becomes the sole owner of dynamically allocated int(10).
  2. The return statement needs to initialize the function's return object.
  3. Because p is an eligible local return object, the language treats it as movable when selecting a constructor.
  4. std::unique_ptr's move constructor is selected. Ownership of the int is transferred to the return object.
  5. p becomes empty.
  6. p is destroyed as the function exits. Since it is empty, it deletes nothing.
  7. The caller initializes its unique_ptr from 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:

  1. Storage for the caller's result is used for the local result object.
  2. The unique_ptr is constructed directly there.
  3. 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_ptr while 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 as std::vector, std::string, and file-wrapper classes.

Take Quiz

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

SituationTypical codeWhat happens
Return an eligible local by valuereturn p;NRVO may occur; otherwise C++ can implicitly move p.
Return a freshly created valuereturn std::make_unique<T>();The result can often be constructed directly in return storage.
Return an explicitly moved localreturn std::move(p);Valid move, but can prevent NRVO for a named local.
Pass ownership to another functiontake(std::move(p));Explicit move is required because this is not the special return context.
Copy ownershipauto q = p;Error for std::unique_ptr; copying would violate exclusive ownership.

std::unique_ptr versus std::shared_ptr

TypeOwnership modelCopyable?Movable?
std::unique_ptr<T>One ownerNoYes
std::shared_ptr<T>Multiple shared ownersYesYes

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.

Take Quiz

Cheat Sheet

  • std::unique_ptr is move-only: it cannot be copied.

  • Return a local unique_ptr with return 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::move when transferring a named unique_ptr to another variable, function, or container.

  • After std::move(p), do not dereference p unless 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);
}
Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more