Skip to content

C++ private 纯虚函数:访问性与多态无关 ​

阅读标准库源码时会在 std::pmr::memory_resource 里看到一种乍看矛盾的写法:三个纯虚函数 do_allocate、do_deallocate、do_is_equal 都声明在 private 区。基类的 private 成员派生类无权访问,那派生类怎么"实现"一个自己看不见的纯虚函数?这样的声明为什么成立、又为什么要这样设计?本文用一个最小示例验证结论:虚分派与访问性完全无关,private 虚函数是成立且常用的设计。

标准库中的实例 ​

std::pmr::memory_resource 的公开接口全是非虚函数,真正的虚函数全部收在 private 区(libstdc++ 实现,字段略):

cpp
class memory_resource {
public:
    void* allocate(size_t bytes, size_t alignment = alignof(max_align_t))
    { return do_allocate(bytes, alignment); }

    void deallocate(void* p, size_t bytes, size_t alignment = alignof(max_align_t))
    { return do_deallocate(p, bytes, alignment); }

    bool is_equal(const memory_resource& other) const noexcept
    { return do_is_equal(other); }

private:
    virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
    virtual void  do_deallocate(void* p, size_t bytes, size_t alignment) = 0;
    virtual bool  do_is_equal(const memory_resource& other) const noexcept = 0;
};

派生类要做的不是"访问"基类的 private 函数,而是按签名提供自己的 override。override 检查只看函数签名与 cv 限定,不看访问性——派生类的实现可以放在自己的任意访问区。

这个结构是 NVI(Non-Virtual Interface,非虚接口)惯用法的标准形态:公开的非虚函数承担接口与不变式(参数校验、统计、日志),具体的差异行为下沉到 private 虚函数作为定制点。外界只能通过 allocate() 进入,无法绕过公开层的约定直接调用 do_allocate。

验证:多态不受访问性影响 ​

cpp
#include <iostream>

class Base {
public:
    virtual ~Base() = default;
    void test1() {                 // 公开入口,内部依次调三个虚函数
        func1();
        func2();
        func3();
    }
    virtual void test() = 0;

protected:
    virtual void func1() = 0;

private:
    virtual void func2() = 0;
    virtual void func3() { std::cout << "Base::func3\n"; }
};

class Derived : public Base {
public:
    void test() override {
        func1();                   // protected,派生类可见
        func2();                   // Derived 自己的 override,可调
        // func3();                // 编译错误:func3 在 Base 中是 private
    }

protected:
    void func1() override { std::cout << "Derived::func1\n"; }

private:
    void func2() override { std::cout << "Derived::func2\n"; }
};

class DDerived : public Derived {
public:
    void test() override {
        func1();
        // func2();                // 编译错误:func2 在 Derived 中是 private
        // func3();                // 编译错误:func3 在 Base 中是 private
    }

protected:
    void func1() override { std::cout << "DDerived::func1\n"; }
};

int main() {
    Base* p1 = new Derived;
    Base* p2 = new DDerived;

    p1->test1();
    p2->test1();

    delete p1;
    delete p2;
}

输出:

text
Derived::func1
Derived::func2
Base::func3
DDerived::func1
Derived::func2
Base::func3

三个观察:

  1. test1() 通过基类指针调用,func2 虽然在两个类中都是 private,仍稳定分派到运行时类型的实现——多态完全正常;
  2. DDerived 没有重写 func2,虚表沿继承链找到 Derived 的实现,输出 Derived::func2;
  3. 访问性挡住的只有派生类内部的直接调用:DDerived::test() 里不能调 func2()(Derived 的 private),也不能调 func3()(Base 的 private),但这不影响任何通过基类指针发生的虚分派。

结论 ​

虚函数的访问性(public/protected/private)只参与访问检查,不参与虚表分派。把纯虚函数声明为 private 的动机通常是 NVI:基类用公开非虚函数固化调用流程,把可定制的步骤藏在 private 虚函数里,既保证外界走统一入口,也给派生类留出 override 的自由。判断某个虚函数该不该 private,与多态机制无关,只取决于这个函数是否应该成为对外(或对派生类)的调用入口。

参考 ​

最近更新

基于 VitePress 构建