找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5101

积分

0

好友

659

主题
发表于 昨天 20:35 | 查看: 7| 回复: 0

从一个简单类说起

PImpl 想必大家都不陌生:把类的私有成员和较重的头文件依赖移到 .cpp 中,放进一个实现类;公开头文件只保留实现类的前向声明,以及指向它的 指针。这样,使用这个类的代码就不必跟着包含那些实现细节。

这样做的好处是接口与实现之间的依赖隔离,减少编译依赖。修改 Impl 的私有成员或内部逻辑时,只要公开接口不变,通常也不需要重新编译所有使用这个类的代码。

看下面这段 Object.h:

// Object.h
#pragma once

#include <string>
#include <vector>

class Object
{
public:
    explicit Object(const std::string& name);

    void addValue(int value);
    int sum() const;

private:
    std::string m_name;
    std::vector<int> m_values;
};

实现也很简单:

// Object.cpp
#include "Object.h"

Object::Object(const std::string& name)
    : m_name(name)
{
}

void Object::addValue(int value)
{
    m_values.push_back(value);
}

int Object::sum() const
{
    int result = 0;

    for (int value : m_values)
    {
        result += value;
    }

    return result;
}

可以像下面这样使用:

// main.cpp
#include "Object.h"

#include <iostream>

int main()
{
    Object object("example");

    object.addValue(10);
    object.addValue(20);

    std::cout << object.sum() << '\n';
}

代码运行没有问题,但 Object.h 中的 std::string 和 std::vector 都属于内部实现。使用方不需要知道数据如何保存,却仍然会通过 Object.h 间接包含这些头文件。

问题在于,如果以后修改 Object 的私有成员,例如增加一个缓存:

private:
    std::string m_name;
    std::vector<int> m_values;

    int m_cachedSum = 0;

即使公共接口完全没有变化,Object.h 仍然发生了修改,依赖它的源文件通常也需要重新编译。对于需要保持二进制兼容的库,新增成员还可能改变对象的大小和布局。

也就是说,只要头文件发生修改,依赖它的所有源文件都可能需要重新编译。PImpl 正是用来解决这类问题的。

前面的 Object 直接保存了字符串和容器,编译器需要知道这些成员的完整类型,才能确定 Object 的大小和布局,所以相应的头文件不能省略。

用指针隔离实现

我们可以把这些数据移到一个单独的实现类 Impl 中,让 Object 只保存指向它的指针。头文件中只需要前向声明 Impl,不必包含字符串和容器的定义:

// Object.h
#pragma once

class Object
{
public:
    explicit Object(const char* name);
    ~Object();

    Object(const Object&) = delete;
    Object& operator=(const Object&) = delete;

    void addValue(int value);
    int sum() const;

private:
    class Impl;
    Impl* m_impl;
};

这里的 class Impl; 是前向声明。编译器知道 Impl 是一个类,但还不知道它有哪些成员。不过,声明 Impl* 不需要知道 Impl 的完整定义,因此不会影响 Object 的大小和布局计算。

Impl 的完整定义放在 Object.cpp 中,原来的名称和整数容器也一起移到这里:

// Object.cpp
#include "Object.h"

#include <string>
#include <vector>

class Object::Impl
{
public:
    explicit Impl(const char* name)
        : m_name(name ? name : "")
    {
    }

    void addValue(int value)
    {
        m_values.push_back(value);
    }

    int sum() const
    {
        int result = 0;

        for (int value : m_values)
        {
            result += value;
        }

        return result;
    }

private:
    std::string m_name;
    std::vector<int> m_values;
};

Object::Object(const char* name)
    : m_impl(new Impl(name))
{
}

Object::~Object()
{
    delete m_impl;
}

void Object::addValue(int value)
{
    m_impl->addValue(value);
}

int Object::sum() const
{
    return m_impl->sum();
}

Object 在构造时创建 Impl,在析构时释放它,对外提供的操作则转发给 Impl。使用方不需要接触实现对象,仍然可以像下面这样使用:

Object object("example");

object.addValue(10);
object.addValue(20);

std::cout << object.sum() << '\n'; // 30

为了移除公开接口对 <string> 的依赖,这里把名称参数改成了 const char*,内部仍然用 std::string 保存,并在构造时复制传入的内容。另外,这个原始指针版本禁用了复制,避免默认复制让两个对象持有同一个 Impl,造成重复释放。

现在,字符串和容器都只出现在 Object.cpp 中。以后增加缓存或更换容器,只要不修改 Object.h,通常就不需要重新编译使用方的源文件。

这就是 PImpl(Pointer to Implementation,指向实现的指针)的基本做法:通过一个指向不完整类型的指针,把具体数据和实现依赖隔离在源文件中。

例如,后面想给 Impl 增加一个缓存成员,只需要在 Object.cpp 中修改它的定义:

private:
    std::string m_name;
    std::vector<int> m_values;

    int m_cachedSum = 0;

虽然 Impl 的定义发生了变化,但 Object.h 没有修改,Object 仍然只保存一个 Impl*。因此,在正常的增量构建中,只需要重新编译 Object.cpp,其他包含 Object.h 的源文件不需要因为这次修改而重新编译。程序仍然需要重新链接,才能使用更新后的实现。

这也是 PImpl 隔离编译依赖的关键:把经常变化的实现留在 .cpp 中,让使用方依赖的头文件保持稳定。

至此,PImpl 思想基本完成。

前置声明放哪

在 PImpl 的实现中,前置声明放置的位置有两种。一种是放在类内部,就像上面这样:

class Object
{
    // 公共接口略

private:
    class Impl;
    Impl* m_impl;
};

这样,Impl 是 Object 的私有嵌套类型,完整名称是 Object::Impl。它不会占用外部命名空间中的名称,也能清楚地表达:这个类型只是 Object 的内部实现,完整定义位于 Object.cpp 中。

对于仅供一个类使用的实现类型,这通常是合适的默认选择。

另一种方式是在类外、命名空间作用域中声明:

class ObjectImpl;

class Object
{
    // 公共接口略

private:
    ObjectImpl* m_impl;
};

如果实现类型需要被多个内部类使用,或者规模较大,需要单独放到内部头文件和源文件中,这种方式也比较方便。独立测试实现类型时,也可以考虑这样的组织方式。

如果没有这些需求,把 Impl 放在类内部通常更直接,也更清楚地表达了它与 Object 的关系。

以上。在云栈社区的技术讨论中,PImpl 也常被当作控制编译期依赖的典型手段。




上一篇:AI 架构图不敢用?Archify 把布局交给 agent,校验交给机器(6.1万星)
下一篇:OpenAI Dots 开源平替:用反检测浏览器给 AI 配隐形上网能力
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-2 00:28 , Processed in 0.481459 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表