unordered_map of class following pImpl idiom using unique_ptr

105 Views Asked by At

Here is a simplified code: https://godbolt.org/z/EnE76xMrP
The pImpl will contain a mutex member which make the pImpl neither copyable nor movable. But the Foo class has unique_ptr of pImpl as member which make Foo movable but not copyable.

class Foo final
{
public:
    Foo();
    ~Foo();
    void DoSomething(const int thread_num);
private:
    int data{};
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

struct Foo::Impl final
{
    mutable std::mutex m_mutex;
};

Foo::Foo()
    : m_impl(std::make_unique<Impl>()) 
{}

Foo::~Foo() {
    std::cout << "Foo dtor \n";
}

For mapping keys to Foo values, I used emplace. But it looks even constructing a pair<int, Foo> is not feasible. Would you please clarify ? Because the compiler message is somewhat cumbersome: no matching function for call to 'std::pair<int, Foo>::pair(int, Foo)'
I tried to use shared_ptr instead of unique_ptr, and it works, but my question is why?

2

There are 2 best solutions below

4
Ahmed AEK On BEST ANSWER

this compiles if you modify the class definition to have an explicitly defined move constructor.

working with Pimpl requires all "exposed" constructors and destructors to be declared in the headers, and implemented in the cpp.

class Foo final
{
public:
    Foo();
    ~Foo();
    Foo(Foo&&) noexcept;
    Foo& operator=(Foo&&) noexcept;
    void DoSomething(const int thread_num);
private:
    int data{};
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

// in the cpp
Foo::Foo(): m_impl(std::make_unique<Impl>()) {};
Foo::~Foo() = default;
Foo::Foo(Foo&&) noexcept = default; 
Foo& Foo::operator=(Foo&&) noexcept = default;

you also need a move assignment operator Foo& operator=(Foo&&) if you ever intend to move assign it.

also note that you should use std::pair<int, Foo> not std::pair<int, Foo&&> as this rvalue Reference would be dangling, hence the SIGSEGV


Edit: the reason you need to explicitly define the move constructor is because unique_ptr move constructor will try to invoke Impl destructor which is only defined inside the cpp file.

0
wearetherobots On

Because you defined explicitly a destructor for Foo, the compiler assumes there is something special about your class and does not generate the move constructor by default. See for example Move constructor

Implicitly-declared move constructor

If no user-defined move constructors are provided for a class type, and all of the following is true:

  • there are no user-declared copy constructors;
  • there are no user-declared copy assignment operators;
  • there are no user-declared move assignment operators;
  • there is no user-declared destructor.

Then the compiler will declare a move constructor as a non-explicit inline public member of its class with the signature T::T(T&&).

If you remove the definition for the constructor your program compiles and runs fine.


class Foo final
{
public:
    Foo();
    //~Foo();
    void DoSomething(const int thread_num);
private:
    int data{};
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

As suggested by the other answer, you could also define the move operations as:

Foo(Foo&&) = default;
Foo& operator=(Foo&&) = default;

(although there should be no concern about header filer vs implementation unit.)