PIMPL & Const & Pointer

PIMPL是C++中一个古董级的隐藏实现的技巧, 通常当我们需要设计一个类而又只希望暴露它的接口时有两种选择. 一是写一个抽象类, 然后继承它; 二是使用PIMPL, 把实现塞到cpp文件里隐藏起来. 其原理在pongba的blog上有很好的阐述. 本文主要讲一下PIMPL实现过程中容易被忽略的const指针问题.

一个简单的PIMPL的实现如下:

// SomeClass.h
class SomeClass {
public:
    // some method
    void Foo() const;
private:
    struct Impl;
    Impl *m_pimpl;
};

// SomeClass.cpp
struct SomeClass::Impl {
    // fields and methods
    void Foo() { ... }
};
// for every method in SomeClass, such as foo()
void SomeClass::Foo() const {
    m_pimpl->Foo();
}

上面的代码有个隐晦的错误, 我后面再说.

在这里我使用struct而不是class来定义Impl, 因为Impl仅存在于cpp文件中, 对其封装完全是多余的. 如果在SomeClass里声明为class, 而实现时使用struct, VS会报warning, 不过貌似没什么大碍.

对于PIMPL通常有两种解释: "pointer to implementation"和"private implementation". 但是无论哪一种, IMPL都应该解释为"implementation", 即"实现", 而不单单是"数据", 所以我认为Impl应该实现SomeClass的每个方法, 然后在SomeClass的方法里要做的仅仅是将任务委托给Impl. 正如上例中的Foo方法. 当SomeClass需要具有某些private方法时, 这样做的好处就体现出来了, 所有实现都在Impl里完成, 不会出现某些一事两地的尴尬.

但是仅仅这样还不够, 通常SomeClass的构造函数可能是这样:

SomeClass::SomeClass() : m_impl(new Impl()) {}

这很明显违反了RAII的原则, 即"Resource Acquisition Is Initialization", 通常的解决办法是使用智能指针, 比如boost::scoped_ptr和boost::shared_ptr, 当然std::auto_ptr也是可以的, 但是需要特别注意SomeClass的拷贝构造函数和赋值操作符的重载. 在scoped_ptr的文档中已经列出了用它来实现PIMPL的例子.

如果用scoped_ptr来实现, 因为其本身是不可拷贝的, 所以编译器会帮你检查出对于SomeClass的非法拷贝, 如果你使用的是默认拷贝构造函数的话, 对于赋值操作也是一样. 但是shared_ptr就不同了, 默认情况下, SomeClass之间的拷贝或赋值会使得多个SomeClass共享同一个Impl. 大多数情况下这是不合理的, 所以下我假定Impl不应该被共享.

解决方法是在SomeClass的拷贝构造函数和赋值操作符重载中调用shared_ptr的unique方法, 此方法要求Impl是可拷贝的.

仅仅这样就足够了吗? 这里给出了一个很好的反例, 其原因就在于

const SomeType *p

SomeType * const p

的区别, 前一种表示p所指向的东西不可改变, 而后一种表示p的内容(即地址)不可改变, 所以SomeClass的const属性传播到Impl的指针上的时候就成了后一种, 于是虽然SomeClass的Foo被声明为Foo, 而它却调用了Impl的非const方法, 于是在Foo调用过后, Impl的状态可能被修改了.

那么给Impl加上一个const的Foo方法可以吗?

很遗憾, 编译器在const和非const方法同时可调用的时候选择非const.

一个简单的解决方法是这样:

void SomeClass::Foo() const {
    static_cast<const *Impl>(m_impl)->Foo();
}

对于使用智能指针的情况也同理.

但是谁能保证总能记得去cast一下呢? Loki的PIMPL子库有一个解决方案, 我没有细看, 大致思想就是引入一个中介, 即利用const属性对于类型和指针的传播方式的不同来构造一个类型把指针包装起来. 这里我给出一个简约的实现:

template<Impl> class Pimpl {
public:
    Pimpl() : m_pImpl(new Impl) {}
    Pimpl(boost::shared_ptr<Impl> impl) : m_impl(impl) {}
    Pimpl(const Pimpl &other) : m_impl(other.m_impl) {
        m_impl.unique();
    }
public:
    Pimpl & operator =(const Pimpl &rhs) {
        if (*this != rhs) {
            m_pImpl = rhs.m_pImpl;
            m_pImpl.unique();
        }
        return *this;
    }
    bool operator ==(const Pimpl &rhs) const { return *m_impl == *(rhs.m_impl); }
    bool operator !=(const Pimpl &rhs) const { return !(*this == rhs); }
    Impl & operator *() { return *m_impl; }
    const Impl & operator *() const { return *m_impl; }
    Impl * operator ->() { return m_impl.get(); }
    const Impl * operator ->() const { return m_impl.get(); }
private:
    boost::shared_ptr<Impl> m_impl;
};

然后SomeClass的声明变成这样:

class SomeClass {
public:
    // some method
    void Foo() const;
private:
    struct Impl;
    Pimpl<Impl> m_pimpl;
};

然后SomeClass.cpp不变, 重新编译, 编译器就会告诉你Foo函数那里出错了. 因为当SomeClass的const属性传播给m_pimpl, 然后在Foo里会调用Pimpl的"->"运算符的const版本, 返回了const Impl*.

这里的前提是Impl不应该被共享, 所以我在Pimpl的拷贝构造函数和赋值操作符重载中都调用了unique. 另外我重载了关系运算符. 这样SomeClass只需使用默认的拷贝构造函数, 赋值操作符和关系运算符即可.

Related posts

Comments 2

  1. saturn wrote at 2010-07-07 20:49

    你好~能不能给我看看ZOJ2866和2865的代码?
    谢谢~

  2. Answeror wrote at 2010-07-08 12:14

    2865的题目在我机器上乱码…2866已经放到blog上了.

Post a Comment

Your email is never published nor shared. Required fields are marked *