pImpl设计如何将文件编译关系降低

Posted milaiko

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pImpl设计如何将文件编译关系降低相关的知识,希望对你有一定的参考价值。

将文件间的编译关系降至最低

其中一个头文件发生改变,其他的都得重新编译。

但你的C++程序中的某个class实现文件做了轻微的修改,(这里的修改指的不是class接口,而是实现,而且只改private成分。因为只有一个class修改,当时输入make后发现这个世界都重新编译和链接了,问题出在哪里?

class Person{
public:
    Person(const std:string &name, const Date& birthday, 
            const Address& addr);
    std::string name() const;
    std::string birthdate() const;
    std::string address() const;

private:
    std::string theName;            //实现细目
    Date theBirthDate;              //实现细目
    Address theAddress;             //实现细目
};

这个代码无法通过编译,因为编译器没有取得其实现代码所用到的classes string,Date和Address的定义式。 这种定义式通常通过#include指示符提供

#include<string>
#include"date.h"
#include"address.h"

但是这么一来Person定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有一个发生了改变,那么每一个含入Person class的文件接得重新编译,任何使用Person class的文件也必须重新编译。这连串的编译会给项目带来灾难。

尝试解决问题一 不把class的实现细目置于class定义式中

namespace std{
    class string;
}
class Date;
class Address;
class Person{
public:
    Person(const std:string& name, const Date& birthday, const Address& addr);
    std::stirng name() const;
    std::stirng bitrhdate() const;
    std::stirng address() const;
    
};

这个想法有两个问题

  • string不是一个class, 它是个typedef(定义为basic_string<char>)。所以针对string而做的前置声明并不正确;正确的声明比较复杂,而且标准头文件不太可能会成为编译瓶颈,所以应该仅仅使用适当的#includes完成目的。
  • 编译器必须在编译期间知道对象的大小,那它如何知道一个Person对象有多大。获得这项信息的唯一方法就是询问class定义式。然而如果class定义式可以合法地不列出实现细目,编译器该如何知道分配空间。

尝试解决问题二 将Person分割 为两个classes

这些问题在java等语言不存在,因为那种语言定义对象时, 编译器只分配足够空间给一个指针(用以指向该对象)使用。

也就是它们将上述代码视同为这个样子。

int main(){
    int x;
    Person* p;
}

也就是将对象实现细目隐藏在一个指针背后。

正对Person我们可以将Person分割成两个classes, 一个只提供接口,一个负责实现该接口。如果负责实现的那个所谓implementation class 取名为PersonImpl。

// Person.h
#include<string>
#include<memory>

class Date;
class Address;
class PersonImpl;

class Person{
public:
    Person(const std:string& name, const Date& birthday, const Address& addr);
    std::stirng name() const;
    std::stirng bitrhdate() const;
    std::stirng address() const;
private:
    std::shared_ptr<PersonImpl> pImpl;
};

这样的设计下,Person的客户就完全与Dates, Addresses以及Persons的实现细目分离了。哪些classes的任何实现修改都不需要Person客户端重新编译。 这就是真正的“接口与实现分离”

这时候有人疑惑,那么pImpl这个实现类到底是怎么实现的?以及pImpl设计是如何做到隐藏private对象的?

为了更好地理解,我们简化例子

// A.h
#ifndef A_H
#define A_H
#include<memory>

class A{
public:
    A();
    ~A();
    void dosomething();
private:
    class A_Impl;
    std::shared_ptr<A_Impl> pImpl;
};
#endif
// A.cpp
#include<stdio.h>
#include"A.h"

class A::Impl{
public:
    int m_count;
    Impl();
    ~Impl();
    void doPrivatesomething();

};
A::Impl::Impl():m_count(0){

}
A::Impl::~Impl(){
}

void A::Impl::doPrivatesomething(){
    printf("count = %d\\n", ++m_count);
}

A::A():pImpl(new Impl){
    
}

A::~A(){}

void A::dosomething(){
    pImpl->doPrivatesomething();
}

在private成员里面只有一个pImpl, 其他private成员变量都被这个指针封装起来,那么当A的用户(也就是使用#include"A.h"的用户),其中A的成员变量发生改变(如新增了private成员, 或者dosomething成员函数需要发生修改,但是接口类(A.h)都不会发生改变,那么在使用了#include"A.h"的代码文件就不需要重新编译,需要重新编译的只有A.cpp。

以上是关于pImpl设计如何将文件编译关系降低的主要内容,如果未能解决你的问题,请参考以下文章

pImpl设计如何将文件编译关系降低

设计模式之PIMPL模式

pimpl idiom

C++ 设计篇之——pimpl 机制

如何使用 Loki 的 Pimpl 实现?

实现 pimpl idiom 时出现链接器错误