2012.08.13 - PImpling with unique_ptr

i've recently found an obvious, but a bit surprising, behavior of C++11's std::unique_ptr<>, when used as an internal holder for Pimpl object. well – in fact the problem is more general, but lets just take one step at a time… consider the following class:

class MyClass
{
public:
  MyClass(void);
  ~MyClass(void);
  void doSth(void);
private:
  class PImpl;
  std::unique_ptr<PImpl> pimpl_;
};

with some basic implementation, say:

class MyClass::PImpl
{
public:
  PImpl(void)
  {
    cout << "MyClass::PImpl::PImpl()" << endl;
  }
  ~PImpl(void)
  {
    cout << "MyClass::PImpl::~PImpl()" << endl;
  }
  void doSth(void)
  {
    cout << "hello PImpl!" << endl;
  }
};
 
MyClass::MyClass(void):
  pimpl_( new PImpl )
{
  cout << "MyClass::MyClass()" << endl;
}
MyClass::~MyClass(void)
{
  cout << "MyClass::~MyClass()" << endl;
}
void MyClass::doSth(void)
{
  pimpl_->doSth();
}

above example “hides” iostream from the user. let us use this class:

int main(void)
{
  MyClass mc;
  return 0;
}

code compiles, and prints:

MyClass::PImpl::PImpl()
MyClass::MyClass()
MyClass::~MyClass()
MyClass::PImpl::~PImpl()

as expected.

the problem

notice however, it was no accident, that we used non-const unique_ptr<> – though this class is noncopyable, we want it to be movable. this pointer makes this for us. and so we write code to use it:

MyClass makeObject(void)
{
  MyClass mc;
  return mc;        // <-- here is the problem - move does not work
}
 
int main(void)
{
  MyClass mc{ makeObject() };
  return 0;
}

and compiler says something like this1):

MyClass.hpp:6:7: note: ‘MyClass::MyClass(const MyClass&)’ is implicitly deleted because the default definition would be ill-formed:
MyClass.hpp:6:7: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = MyClass::PImpl; _Dp = std::default_delete<MyClass::PImpl>; std::unique_ptr<_Tp, _Dp> = std::unique_ptr<MyClass::PImpl>]’

looks like that move c-tor was not taken into the consideration. copy c-tor is tried to be used instead, and it may not work (explicitly blocked by the unique_ptr<>). this is when i was shocked – what is the problem? move c-tor is implicitly generated by the compiler. yes, unless it would be ill-formed…

the solution

the problem is, that on the MyClass.hpp header level compiler does NOT know how to use unique_ptr<PImpl> instances (this is why we moved MyClass's d-tor to the implementation file in the first place). since PImpl is only forward-declared, no code can be generated automatically. thus we need to provide move c-tor and move assignment operator ourselves, just like we did for c-tor and d-tor:

// MyClass.hpp:
MyClass(MyClass&& other);
MyClass& operator=(MyClass&& other);
 
// MyClass.cpp:
MyClass::MyClass(MyClass&& other):
  pimpl_( std::move( other.pimpl_ ) )
{ }
MyClass& MyClass::operator=(MyClass&& other)
{
  MyClass tmp( std::move(other) );
  swap(pimpl_, tmp.pimpl_);
  return *this;
}

this way we allow compiler to generate all the required methods of unique_ptr<PImpl>, when needed.

to toy around with above mentioned issue use this sample code.

afterword

as mentioned at the beginning, the problem is more general. in fact it applies to all templates, instantiated with forward-declaration classes. in order to use it at all as such, their size must be known, thus they can hold pointer or reference to the instances of the parameter, at most. the thing is that problem is easily found when using this template as a member of other class, when the compiler wants to use (implicit) method, that requires full class body, of the template argument. PImpl is an excellent place to spot such an issue then. just keep in mind, it is far from last place you can find this issue.

when i finally realized why my code does not compile i was amazed i haven't seen it in the first place. it seems so obvious, yet there are few things to notice first… probably the most misleading part was the compiler error message. i hope Clang's C++11-compliant release will make life simpler, by hitting the core of the problem, not just scratching its surface. for now keep in mind Paulo Coelho's advise: Be brave. Take risks. Nothing can substitute experience.

ps

maybe a bit off topic, but worth mentioning, when talking about unique_ptr<>, is the fact, that unique_ptr<>, in its default deleter, has a protection against destroying forward-declared objects – something like boost's checked_delete<>. this makes destruction of forward-declared-only type instances impossible, preventing hard to track errors. nice work! :)

1)
actual output is from gcc 4.7
blog/2012/08/13/1.txt · Last modified: 2021/06/15 20:09 by 127.0.0.1
Back to top
Valid CSS Driven by DokuWiki Recent changes RSS feed Valid XHTML 1.0