harmic
harmic

Reputation: 30577

Cannot return std::unique_ptr from function without move

I was updating some old code that uses auto_ptr to use unique_ptr instead. It was mostly a search and replace job, but I found I was getting a compilation error in one place where the code returns a unique_ptr.

Here is an example that illustrates the issue:

#include <string>
#include <iostream>
#include <memory>
#include <utility>

using namespace std;

struct Foo {
    unique_ptr<string> us;

    Foo() {
        this->us = unique_ptr<string>(new string("hello"));
    }

    unique_ptr<string> bar() {
        return this->us;
    }

};


int main(int argc, const char **argv) {

    Foo foo;
    unique_ptr<string> s = foo.bar();
    cout << *s << endl;

}

When I compile that, I get this:

t1.cpp: In member function ‘std::unique_ptr<std::basic_string<char> > Foo::bar()’:
t1.cpp:17:16: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::basic_string<char>; _Dp = std::default_delete<std::basic_string<char> >]’
   return this->us;
                ^~
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/memory:80:0,
                 from t1.cpp:4:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/unique_ptr.h:388:7: note: declared here
       unique_ptr(const unique_ptr&) = delete;
       ^~~~~~~~~~

If I alter the line with the error to specify a move like this:

        return move(this->us);

then it works.

I have found multiple references indicating that a move should not be required - for example this SO question, and these guidelines from the chromium project.

My question is: why does the move need to be explicitly specified in this case? Is it related to fact that I am returning the value of an instance variable somehow?

Apologies in advance if this is a dupe - I feel sure it would have been asked before, but I struggle to come up with search terms that find it.

Upvotes: 5

Views: 7099

Answers (5)

PeterT
PeterT

Reputation: 8284

This shows the exact danger of auto_ptr and why it was removed.

If you imagine calling the function twice you can see what the issue is.

auto_ptr<string> Foo::bar() {
    return this->us;
}

int func() {
  Foo foo;
  auto_ptr<string> a = foo.bar();
  auto_ptr<string> b = foo.bar();//uh oh, null ptr
}

If you like this behavior, you can emulate it very easily with unique_ptr

#include <string>
#include <iostream>
#include <memory>
#include <utility>

using namespace std;

struct Foo {
    unique_ptr<string> us;

    Foo() {
        this->us = unique_ptr<string>(new string("hello"));
    }

    unique_ptr<string> bar() {
        unique_ptr<string> local_us(us.release());
        return local_us;
    }

};


int main(int argc, const char **argv) {

    Foo foo;
    unique_ptr<string> a = foo.bar();
    unique_ptr<string> b = foo.bar();//b is a nullptr
    cout << *s << endl;

}

Upvotes: 2

Alan Birtles
Alan Birtles

Reputation: 36379

Used correctly std::auto_ptr has the same purpose as std::unique_ptr, it holds a unique reference to a single object which is deleted on destruction of the pointer.

As std::auto_ptr predates move semantics it has a copy constructor which behaves like a move constructor. It is very easy to accidentally copy a std::auto_ptr and not realise your original std::auto_ptr is now empty.

As std::unqiue_ptr is only moveable and not copyable this situation is avoided. You have to explicitly give up your ownership of the pointer using std::move.

The exception to this is with local stack allocated std::unique_ptrs, these would be destroyed at the end of the current scope anyway so the language allows you to return a std::unique_ptr without using std::move. As your us variable is not a local variable you aren't allowed to return it directly to protect you from accidentally releasing ownership.

Upvotes: 2

Maxim Egorushkin
Maxim Egorushkin

Reputation: 136208

You are trying to return Foo::us member variable of type std::unique_ptr<>. The sole purpose of std::unique_ptr<> is to be non-copyable but moveable. This is why that return requires std::move which resets Foo::us member variable to null.

bar function should probably return that std::string by value or a const reference:

string const& bar() const {
    return *this->us;
}

And then:

int main() {
    Foo foo;
    string const& s = foo.bar();
    cout << s << endl;
}

Upvotes: 3

Christophe
Christophe

Reputation: 73366

It's all about semantics. A unique_ptr is a unique ownership, so it would be either returned or in us, but not both. The rules of the standard fortunately protect you against this misunderstanding.

What you can do is return a temporary unique_ptr (which will be moved):

unique_ptr<string> bar() {
    return make_unique<string>("hello, world");
}

But if you need a copy of the unique_ptr in several places, which seems to be the case according to your main(), you are not looking for a unique but a shared ownership. So you should go to a shared_ptr:

struct Foo {
    shared_ptr<string> us;

    Foo() {
        us = make_shared<string>("hello, world");
    }
    shared_ptr<string> bar() {
        return us;
    }
};

int main(int argc, const char **argv) {
    Foo foo;
    auto s = foo.bar();
    cout << *s << endl;
}

Upvotes: 9

The references you found all mention that the object returned must be a function local variable. You return a member. Member variables will not be implicitly treated as rvalues when returned.

It's not without reason. A class usually doesn't want to lose ownership of its resources unless done explicitly.

A function local is about to die anyway when the function returns, so it's a worthwhile optimization to let the compiler implicitly cannibalize it.

Upvotes: 8

Related Questions