Unwanted copy constructor call when creating a shared_ptr

168 Views Asked by At

While doing some coding practice, I encountered the following error when attempting to pass ownership of an optional std::unique_ptr to a std::shared_ptr:

/usr/include/c++/11/ext/new_allocator.h:162:11: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = unsigned char; _Dp = std::default_delete]’
  162 |         { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
      |           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/c++/11/memory:76,
                 from main.cpp:2:
/usr/include/c++/11/bits/unique_ptr.h:468:7: note: declared here
  468 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~

This was the particular snippet that resulted in my error (apologies for the nonsensical code):

#include <optional>
#include <memory>

class test_class
{
    public:
        struct options
        {
            options();
            std::optional<std::unique_ptr<uint8_t>> data;
        };
    
        test_class(const test_class::options &options = test_class::options());

    private:
        std::shared_ptr<std::unique_ptr<uint8_t>> _data;
};

test_class::test_class(
    const test_class::options &options)
{
    if (options.data.has_value())
    {
        _data = std::make_shared<std::unique_ptr<uint8_t>>(std::move(options.data.value()));
    }
}

I assumed that I would've bypassed any copy constructors by using the std::move call, but it seems like I may be inadvertently calling it anyway.

Furthermore, I ran this similar snippet of code, but there was no error:

test_class::test_class(
    const test_class::options &options)
{
    std::optional<std::unique_ptr<uint8_t>> data;

    if (data.has_value())
    {
        _data = std::make_shared<std::unique_ptr<uint8_t>>(std::move(data.value()));
    }
}

Can anyone explain why this is happening? Why is the first snippet wrong? Is there some C++ fundamental I am misunderstanding? Why does the second snippet not also fail?

Thanks!

2

There are 2 best solutions below

1
Damian On BEST ANSWER

As stated in a comment above, this behavior is a result of the test_class::options argument of the constructor being const. The use of std::move is intended to cast options.data.value to a const std::unique_ptr<uint8_t>&&. However, due to the const qualifier, it prevents the use of the move constructor. Consequently, the code attempts to construct a new unique_ptr from this casted value but falls back to using the copy constructor instead of the move constructor.

I corrected this as follows:

#include <optional>
#include <memory>

class test_class
{
    public:
        struct options
        {
            options();
            
            std::optional<std::unique_ptr<uint8_t>> data;
        };
        
        test_class(test_class::options &&options =  = dst_file::options());

    private:
        std::shared_ptr<std::unique_ptr<uint8_t>> _data;
};

test_class::test_class(
    const test_class::options &options)
{
   
}

test_class::test_class(
    test_class::options &&options)
{
    if (options.data.has_value())
    {
        _data = std::make_shared<std::unique_ptr<uint8_t>>(std::move(options.data.value()));
    }
}

Since test_class::options is now an "rvalue reference" (as denoted by the &&), it will invoke the move constructor by default.

0
Peter - Reinstate Monica On

Your complicated nested pointers obscure the underlying issue somewhat. Ownership is not really the issue here, and there is no issue creating a shared_ptr from a unique_ptr. The error you get concerns solely the unique_ptr. Here the mentioned "obscuring" becomes noticeable because the error is hard to understand. But if I cut all the fluff like std:: and template parameters, it boils down to

use of deleted function unique_ptr::unique_ptr(const unique_ptr &),

which is the copy constructor. The copy constructor for unique_ptr is deleted because a unique_ptr cannot be copied; it is, after all, unique.

But why is the copy constructor selected at all by overload resolution? Because the declared move constructor moves from an rvalue reference which is not const. A const rvalue reference is simply not a match. The fallback then is an attempt to use the copy constructor since a const reference can be bound to a constant rvalue reference; but that one is, as we said, deleted for good reasons.

Below is code which demonstrates the workings with a trivial class S. Your constructor case is demonstrated with test_class, but the easiest demonstration is actually the last line in main(): You simply cannot construct an S from a const rref. The case that an S may be a member of a different class and the construction is part of that class's initialization is not really important to the problem.

#include <utility> // std::move

using namespace std;

// this takes on the role of unique_ptr in your example
struct S 
{ 
    S() = default;

    // [1] enable the copy constructor ... 
    //S(const S&) = default;

    // [2] ... or the move constructor from rref to const;
    //S(const S&&) {}

    // [3] OR remove the move constructor; that will un-delete the copy ctor
    S(S&&) = default;
};

class test_class
{
    public:
        S _s;
 
        // Does not work because no constructor matches. In the order of 
        // resolution attempts: 
        // 1. The best match, a constructor taking a const rref argument, is not declared, see [2]
        // 2. The declared regular move constructor [3] taking a non-const rref argument does not match;
        // 3. The regular copy constructor taking a const ref argument
        //    is implicitly deleted because we declared a move constructor [3].
        //    We could explicitly define it, [1]
        // test_class(const S &sArg): _s{move(sArg)} {}
 };

int main()
{
    const S cs;
     // move actually works like a charm but produces an rref to const.
    const S &&s_rref{move(cs)};

    // construction from a const rref is not defined.
    // S s{move(cs)};
}

Here is a version on godbolt.