views:

274

answers:

2

I am maintaining a project that can take a considerable time to build so am trying to reduce dependencies where possible. Some of the classes could make use if the pImpl idiom and I want to make sure I do this correctly and that the classes will play nicely with the STL (especially containers.) Here is a sample of what I plan to do - does this look OK? I am using std::auto_ptr for the implementation pointer - is this acceptable? Would using a boost::shared_ptr be a better idea?

Here is some code for a SampleImpl class that uses classes called Foo and Bar:

// SampleImpl.h
#ifndef SAMPLEIMPL_H
#define SAMPLEIMPL_H

#include <memory>

// Forward references
class Foo;
class Bar;

class SampleImpl
{
public:
    // Default constructor
    SampleImpl();
    // Full constructor
    SampleImpl(const Foo& foo, const Bar& bar);
    // Copy constructor
    SampleImpl(const SampleImpl& SampleImpl);
    // Required for std::auto_ptr?
    ~SampleImpl();
    // Assignment operator
    SampleImpl& operator=(const SampleImpl& rhs);
    // Equality operator
    bool operator==(const SampleImpl& rhs) const;
    // Inequality operator
    bool operator!=(const SampleImpl& rhs) const;

    // Accessors
    Foo foo() const;
    Bar bar() const;

private:
    // Implementation forward reference
    struct Impl;
    // Implementation ptr
    std::auto_ptr<Impl> impl_;
};

#endif // SAMPLEIMPL_H

// SampleImpl.cpp
#include "SampleImpl.h"
#include "Foo.h"
#include "Bar.h"

// Implementation definition
struct SampleImpl::Impl
{
    Foo foo_;
    Bar bar_;

    // Default constructor
    Impl()
    {
    }

    // Full constructor
    Impl(const Foo& foo, const Bar& bar) :
        foo_(foo),
        bar_(bar)
    {
    }
};

SampleImpl::SampleImpl() :
    impl_(new Impl)
{
}

SampleImpl::SampleImpl(const Foo& foo, const Bar& bar) :
    impl_(new Impl(foo, bar))
{
}

SampleImpl::SampleImpl(const SampleImpl& sample) :
    impl_(new Impl(*sample.impl_))
{
}

SampleImpl& SampleImpl::operator=(const SampleImpl& rhs)
{
    if (this != &rhs)
    {
        *impl_ = *rhs.impl_;
    }
    return *this;
}

bool SampleImpl::operator==(const SampleImpl& rhs) const
{
    return  impl_->foo_ == rhs.impl_->foo_ &&
        impl_->bar_ == rhs.impl_->bar_;
}

bool SampleImpl::operator!=(const SampleImpl& rhs) const
{
    return !(*this == rhs);
}

SampleImpl::~SampleImpl()
{
}

Foo SampleImpl::foo() const
{
    return impl_->foo_;
}

Bar SampleImpl::bar() const
{
    return impl_->bar_;
}
+3  A: 

You should consider using copy-and-swap for assignment if it's possible that Foo or Bar might throw as they're being copied. Without seeing the definitions of those classes, it's not possible to say whether they can or not. Without seeing their published interface, it's not possible to say whether they will in future change to do so, without you realising.

As jalf says, using auto_ptr is slightly dangerous. It doesn't behave the way you want on copy or assignment. At a quick look, I don't think your code ever allows the impl_ member to be copied or assigned, so it's probably OK.

If you can use scoped_ptr, though, then the compiler will do that tricky job for you of checking that it's never wrongly modified. const might be tempting, but then you can't swap.

Steve Jessop
If I use `boost::scoped_ptr` can I drop the empty destructor from my class?
Rob
I hadn't noticed the empty destructor, actually. What does it achieve in the current code - isn't it identical to having no destructor at all and letting the compiler generate the default? I guess the rule of 3 says you need to implement the destructor, but actually in this case I think the rule is wrong. The default destructor is fine, because you aren't actually cloning any resources in operator=, just modifying the one you've already got.
Steve Jessop
`boost::scoped_ptr` doesn't work unless I supply a destructor (I get 'use of undefined type' and 'deletion of pointer to incomplete type 'SampleImpl::Impl'; no destructor called' errors. However, `boost::shared_ptr` doesn't require a destructor.
Rob
@Steve. Without the destructor I get compilation errors if I use `boost::scoped_ptr`. I had it there for `std::auto_ptr` because I'm sure I once read it is required (might of been Meyers.)
Rob
Oh, I see. Yes, it's to do with the default destructor being generated without a definition of Impl, whereas if you define the destructor in the cpp file, then Impl is a complete type and its destructor is visible. I don't know why shared_ptr is different, sorry.
Steve Jessop
Ah, I now know why shared_ptr is different, its because shared_ptr doesn't reference the payload's destructor in its destructor, it references it in its constructor. auto_ptr and scoped_ptr don't have configurable deallocators, so they reference the payload's destructor in their destructors. http://stackoverflow.com/questions/311166/stdautoptr-or-boostsharedptr-for-pimpl-idiom
Steve Jessop
A: 

There are a couple of problems with the Pimpl.

First of all, though not evident: if you use Pimpl, you will have to define the copy constructor / assignment operator and destructor (now known as "Dreaded 3")

You can ease that by creating a nice template class with the proper semantic.

The problem is that if the compiler sets on defining one of the "Dreaded 3" for you, because you had used forward declaration, it does know how to call the "Dreaded 3" of the object forward declared...

Most surprising: it seems to work with std::auto_ptr most of the times, but you'll have unexpected memory leaks because the delete does not work. If you use a custom template class though, the compiler will complain that it cannot find the needed operator (at least, that's my experience with gcc 3.4.2).

As a bonus, my own pimpl class:

template <class T>
class pimpl
{
public:
  /**
   * Types
   */
  typedef const T const_value;
  typedef T* pointer;
  typedef const T* const_pointer;
  typedef T& reference;
  typedef const T& const_reference;

  /**
   * Gang of Four
   */
  pimpl() : m_value(new T) {}
  explicit pimpl(const_reference v) : m_value(new T(v)) {}

  pimpl(const pimpl& rhs) : m_value(new T(*(rhs.m_value))) {}

  pimpl& operator=(const pimpl& rhs)
  {
    pimpl tmp(rhs);
    swap(tmp);
    return *this;
  } // operator=

  ~pimpl() { delete m_value; }

  void swap(pimpl& rhs)
  {
    pointer temp(rhs.m_value);
    rhs.m_value = m_value;
    m_value = temp;
  } // swap

  /**
   * Data access
   */
  pointer get() { return m_value; }
  const_pointer get() const { return m_value; }

  reference operator*() { return *m_value; }
  const_reference operator*() const { return *m_value; }

  pointer operator->() { return m_value; }
  const_pointer operator->() const { return m_value; }

private:
  pointer m_value;
}; // class pimpl<T>

// Swap
template <class T>
void swap(pimpl<T>& lhs, pimpl<T>& rhs) { lhs.swap(rhs); }

Not much considering boost (especially for the cast issues), but there are some niceties:

  • proper copy semantic (ie deep)
  • proper const propagation

You still have to write the "Dreaded 3". but at least you can treat it with value semantic.

Matthieu M.