C++中的PIMPL封装技术
C++的头文件存在设计缺陷,你不得不在头文件中把类的私有成员变量和方法定义也写出来,这显然不符合接口实现原则,那么怎样更好地封装接口呢?
在C++中我们常常定义一些头文件,在头文件编写函数/类的定义,在cpp文件中去实现,这样头文件就可以被多个地方复用,如下:
// Code in Printer.h
#pragma once
#include <string>
class Printer
{
public:
std::string tag;
Printer(const std::string& tag);
void printSomething();
void printTestPage();
private:
void printStep1();
void printStep2();
void printStep3();
};
// Code in Printer.cpp
#include "Printer.h"
#include <iostream>
Printer::Printer(const std::string& tag) :tag(tag) {}
void Printer::printStep1() {
std::cout << tag << ": step1" << std::endl;
}
void Printer::printStep2() {
std::cout << tag << ": step2" << std::endl;
}
void Printer::printStep3() {
std::cout << tag << ": step3" << std::endl;
}
void Printer::printSomething() {
std::cout << tag << ": printing something" << std::endl;
printStep1();
printStep2();
printStep3();
}
void Printer::printTestPage() {
std::cout << tag << ": printing testpage" << std::endl;
printStep1();
printStep3();
}
然而,稍微思考我们就能发现上述代码的问题:printStep1()
、printStep2()
和printStep3()
都是类的私有成员方法,但是却仍然出现在了header文件中,假如类内部逻辑有所修改,我们还要去找客户把他们的所有引入这个Printer.h
的文件重新编译,这太荒谬了。
这个算是当前C++版本的缺陷,C++需要在头文件中定义类时就确定类的数据大小,因此只能在这里就把所有私有方法定义出来。为了解决这个问题,可以有几个思路。
私有方法定义为工具方法
既然类的成员函数不得不定义在头文件中,那么只要不是成员函数,当然就不用受这个限制了。
// Code in Printer.h
#pragma once
#include <string>
class Printer
{
public:
std::string tag;
Printer(const std::string& tag);
void printSomething();
void printTestPage();
};
// Code in Printer.cpp
#include "Printer.h"
#include <iostream>
Printer::Printer(const std::string& tag) :tag(tag) {}
void printStep1(Printer* printer) {
std::cout << printer->tag << ": step1" << std::endl;
}
void printStep2(Printer* printer) {
std::cout << printer->tag << ": step2" << std::endl;
}
void printStep3(Printer* printer) {
std::cout << printer->tag << ": step3" << std::endl;
}
void Printer::printSomething() {
std::cout << tag << ": printing something" << std::endl;
printStep1(this);
printStep2(this);
printStep3(this);
}
void Printer::printTestPage() {
std::cout << tag << ": printing testpage" << std::endl;
printStep1(this);
printStep3(this);
}
Pimpl封装
如果类的私有代码比较复杂,涉及一些状态的保存等,就可以用上所谓的Pimpl封装技术了。
// Code in Printer.h
#pragma once
#include <string>
class Printer
{
public:
std::string tag;
Printer(const std::string& tag);
~Printer();
void printSomething();
void printTestPage();
private:
class Impl;
Impl* pImpl;
};
#include "Printer.h"
#include <iostream>
class Printer::Impl {
public:
Printer* thisRef = nullptr;
Impl(Printer* thisRef) : thisRef(thisRef) {}
void printStep1() {
std::cout << thisRef->tag << ": step1" << std::endl;
}
void printStep2() {
std::cout << thisRef->tag << ": step2" << std::endl;
}
void printStep3() {
std::cout << thisRef->tag << ": step3" << std::endl;
}
};
// Code in Printer.cpp
Printer::Printer(const std::string& tag) :tag(tag) {
pImpl = new Printer::Impl(this);
}
Printer::~Printer() {
delete pImpl;
pImpl = nullptr;
}
void Printer::printSomething() {
std::cout << tag << ": printing something" << std::endl;
pImpl->printStep1();
pImpl->printStep2();
pImpl->printStep3();
}
void Printer::printTestPage() {
std::cout << tag << ": printing testpage" << std::endl;
pImpl->printStep1();
pImpl->printStep3();
}
PIMPL意为“指向实现的指针”,不是一个标准的设计模式,但可以看作是桥接模式的一个特例。我们在头文件中定义的Impl类只有一个名字,是一个不完整类型,我们能对一个不完整类型做的事情很少,无法实例化,但创建一个指针还是可以的。Printer的实现被桥接到Impl类,这样就解决了Printer类的定义要在头文件完整给出的尴尬。当然,这个例子中我是用原始指针,以及存在没有实现拷贝构造函数等问题,这些不是本文讨论重点。
现在我们有了一把锤子,但不能看什么都是钉子。尤其是只被使用一次的模块内部类,强行搞Pimpl只能是给自己找罪受。建议是,只在模块的边界,或者做SDK开发时才大量用Pimpl封装保证接口的稳定性。
最后修改于 2024-09-05