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