什么是Pimpl惯用法
Pimpl是一种C++编码技巧,凭借这样一种技巧,你可以将类中的数据成员换成一个指向包含具体实现类(或结构体)的指针,并将主类中的数据成员移到实现类中,通过指针间接访问这些数据成员,这样的目的是通过减少在类实现和类使用者之间的编译依赖来减少编译时间(是前向声明
很好的实践)。
接下来我们编写一个简单的Person
类来了解Pimpl惯用法书写的细节。
代码展示
Person.hpp
#pragma once
#include <memory>
class Person
{
public:
Person();
private:
class PersonImpl;
std::unique_ptr<PersonImpl> personImplPtr;
};
在L8
前向声明了PersonImpl
类型,只有一个指向PersonImpl
的unique_ptr
类型的成员变量personImplPtr
。
Person.cpp
#include "Person.hpp"
#include <string>
#include <vector>
#include <unordered_map>
// ------PersonImpl------
class Person::PersonImpl
{
public:
PersonImpl()
: name{"zhangsan"}, id{20}, sex{'M'}, score{{"math", 90.0}, {"english", 78.0}} {}
private:
const std::string name;
int id;
char sex;
std::unordered_map<std::string, double> score;
};
// ------Person------
Person::Person(): personImplPtr{std::make_unique<PersonImpl>()}
{
}
在源文件中首先我们定义了Person::PersonImpl
类型,其中包含多种类型的成员变量,然后在Person::Person()
中用std::make_unique
实例化。
main.cpp
#include "Person.hpp"
int main(int argc, char*argv[])
{
Person p;
return 0;
}
在main()
中我们构造了一个普通的Person
类型的变量,看起来人畜无害,但是当我们编译源文件时,则会出现以下类似错误:
In file included from /usr/include/c++/11/memory:76,
from Person.hpp:2,
from main.cc:1:
/usr/include/c++/11/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Person::PersonImpl]’:
/usr/include/c++/11/bits/unique_ptr.h:361:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Person::PersonImpl; _Dp = std::default_delete<Person::PersonImpl>]’
Person.hpp:3:7: required from here
/usr/include/c++/11/bits/unique_ptr.h:83:23: error: invalid application of ‘sizeof’ to incomplete type ‘Person::PersonImpl’
83 | static_assert(sizeof(_Tp)>0,
| ^~~~~~~~~~~
错误分析
大致就是说你使用的Person::PersonImpl
类型是不完整的,导致unique_ptr.h:83:23
断言失败,程序退出,让我们看看L83
附近的代码:
unique_ptr.h:L62~L83
template<typename _Tp>
struct default_delete
{
/// Default constructor
constexpr default_delete() noexcept = default;
/** @brief Converting constructor.
*
* Allows conversion from a deleter for objects of another type, `_Up`,
* only if `_Up*` is convertible to `_Tp*`.
*/
template<typename _Up,
typename = _Require<is_convertible<_Up*, _Tp*>>>
default_delete(const default_delete<_Up>&) noexcept { }
/// Calls `delete __ptr`
void
operator()(_Tp* __ptr) const
{
static_assert(!is_void<_Tp>::value,
"can't delete pointer to incomplete type");
static_assert(sizeof(_Tp)>0,
"can't delete pointer to incomplete type");
delete __ptr;
}
};
这是一个叫做default_delete
的模板类,重载了调用运算符,这是被当作unique_ptr
的默认删除器来使用的,在其中对_Tp
类型进行大小判断,_Tp
类型及就是我们要构造的Person::PersonImpl
类型,这里为什么会失败呢?其实是由析构函数
引起的。当main()
中定义的变量p
离开作用域准备析构时,会调用自己的析构函数,但是Person
类型中并没有显示定义析构函数,所以编译器会生成一个默认版本的析构函数,且通常是inline
的。在析构函数中会依次调用各个成员变量的析构函数,这里会调用std::unique_ptr
的析构函数,这个函数中会调用上述的default_delete
来对所申请的资源进行delete
,但在delete
之前会static_assert
,所以就出现了上述的错误。
解决方案
在Person.hpp
中显示声明析构函数,在Person.cpp
中定义析构函数(+
代表新添加的行)。移动构造
和移动赋值
同理。
Person.hpp
#pragma once
#include <memory>
class Person
{
public:
Person();
Person(Person&& rhs); // +
Person& operator=(Person&& rhs); // +
~Person(); // +
private:
class PersonImpl;
std::unique_ptr<PersonImpl> personImplPtr;
};
Person.cpp
#include "Person.hpp"
#include <string>
#include <vector>
#include <iostream>
#include <unordered_map>
// ------PersonImpl------
class Person::PersonImpl
{
public:
PersonImpl()
: name{"zhangsan"}, id{20}, sex{'M'}, score{{"math", 90.0}, {"english", 78.0}}
{std::cout << "Person::PersonImpl()" << std::endl;}
~PersonImpl(){std::cout << "Person::~PersonImpl()" << std::endl;} // +
private:
const std::string name;
int id;
char sex;
std::unordered_map<std::string, double> score;
};
// ------Person------
Person::Person()
: personImplPtr{std::make_unique<PersonImpl>()} {std::cout << "Person()" << std::endl;}
Person::Person(Person&& rhs)
: personImplPtr{std::move(rhs.personImplPtr)} {std::cout << "Person(Person&&)" << std::endl;} // +
Person& Person::operator=(Person&& rhs) // +
{ // +
std::cout << "Person& operator(Person&&)" << std::endl; // +
personImplPtr = std::move(rhs.personImplPtr); // +
return *this; // +
} // +
Person::~Person() {std::cout << "~Person()" << std::endl;} // +
main.cpp
#include "Person.hpp"
int main(int argc, char*argv[])
{
Person p;
Person p1{std::move(p)}; // + 移动构造
Person p2; // +
p2 = std::move(p1); // + 移动赋值
return 0;
}
这样的话就不会报错,输出结果如下:
Person::PersonImpl() # p.impl
Person() # p本身
Person(Person&&) # p1移动构造
Person::PersonImpl() # p2.impl
Person() # p2本身
Person& operator(Person&&) # p2移动赋值
Person::~PersonImpl() # p2.impl析构
~Person() # p2本身析构
Person::~PersonImpl() # p1.impl析构
~Person() # p1本身析构
~Person() # p析构
总结
- Pimpl惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
- 对于
std::unique_ptr
类型的pImpl指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。养成良好的编程习惯,不依赖编译器为你生成任何函数,就算不用这个函数,也要在头文件中显示声明,源文件中做空实现
。 - 以上的建议只适用于
std::unique_ptr
,不适用于std::shared_ptr
。
参考文献
https://cntransgroup.github.io/EffectiveModernCppChinese/3.MovingToModernCpp/item17.html
https://cntransgroup.github.io/EffectiveModernCppChinese/4.SmartPointers/item22.html