C++ CRTP无虚函数场景下仅允许直接子类访问父类方法
问题描述
假设定义了带有print()方法的Test类型,以及继承自Test的SubTest类型。现在新增继承自SubTest的SubSubTest类型,需要实现以下访问控制规则:
- 允许
SubTest访问Test的print()方法 - 禁止
SubSubTest访问Test的print()方法
本质是实现部分方法仅对直接子类开放的访问控制,所有实现基于CRTP完成,不使用虚函数。
简化示例代码如下:
class Test { protected: void print() { std::cout << "in Test" << std::endl; } }; class SubTest: public Test { void print() { Test::print(); } }; class SubSubTest: public SubTest { void print() { Test::print(); // 此处需要编译报错,禁止访问 } };
实际业务场景示例
实现目标为使用直接调用替代回调机制,完整代码如下:
#include <iostream> enum class Type { whatever, }; template <typename T> class SSLsocket; struct Socket { void send(char *buff, int size) { std::cout << "Socket send" << std::endl; } void on_message(char *buff, int size); Type type; // 为简化示例设为public private: template <typename T> void dispath_message(SSLsocket<T> *sock, char *buff, int size); int fd; }; template <typename T> class SSLsocket { public: void send(char *buff, int size) { std::cout << "SSLsocket send" << std::endl; socket.send(buff, size); } private: friend Socket; void on_message(char *buff, int size) { std::cout << "SSLsocket on_message" << std::endl; reinterpret_cast<T *>(this)->on_message(buff, size); } private: Socket socket; }; template <typename T> class websocket : public SSLsocket<websocket<T>> { public: void send(char *buffer, int size) { std::cout << "websocket send" << std::endl; SSLsocket<websocket<T>>::send(buffer, size); } private: friend SSLsocket<websocket<T>>;; void on_message(char *buff, int size) { std::cout << "websocket on_message" << std::endl; reinterpret_cast<T *>(this)->on_message(buff, size); } }; class whatever : public websocket<whatever> { public: void send(char *buffer, int size) { std::cout << "whatever send" << std::endl; websocket<whatever>::send(buffer, size); // 此处必须禁止调用 // SSLsocket<websocket<whatever>>::send(buffer, size); } private: friend websocket<whatever>; void on_message(char *buff, int size) { std::cout << "whatever on_message" << std::endl; send(nullptr, 0); } }; void Socket::on_message(char *buff, int size) { std::cout << "socket on_message" << std::endl; switch (type) { case Type::whatever: { auto *w = reinterpret_cast<whatever *>(this); dispath_message(w, buff, size); break; } } } template <typename T> void Socket::dispath_message(SSLsocket<T> *sock, char *buff, int size) { sock->on_message(buff, size); } int main() { // 类中无额外成员,为简化示例仅构造Socket类后续做类型转换 Socket sock; sock.type = Type::whatever; sock.on_message(nullptr, 0); }
场景约束
Socket类型需要感知所有final类型(例如示例中的whatever类型)完成首次类型转换- 已知该写法不属于常规推荐实践,但后续新增类型数量极少,可通过独立文件+编译警告约束,保证修改经过谨慎校验
- 当前核心问题:
whatever类可以直接调用SSLsocket类的send函数,不符合设计预期
调用链路规则
on_message调用方向:从Socket类逐层向下传递到最终final类send调用方向:从final类逐层向上传递到Socket类
实现方案
使用编译期通行证(PassKey)模式实现,零运行时开销,完全兼容CRTP、无虚函数要求。
核心原理
- 每个CRTP基类定义一个空的通行证类型,构造函数设为私有,仅将当前CRTP的模板参数(即直接子类)声明为友元
- 需要限制仅直接子类访问的方法,增加一个该通行证类型的形参(不需要传实参值,仅做编译期权限校验)
- 间接子类不是通行证类型的友元,无法构造通行证实例,因此无法调用对应受限方法
简化示例改造
#include <iostream> // CRTP基类 template <typename Derived> class Test { protected: // 通行证类型,仅直接子类Derived可构造 class PrintAccessKey { friend Derived; PrintAccessKey() = default; }; void print(PrintAccessKey) { std::cout << "in Test" << std::endl; } }; // 直接子类 class SubTest: public Test<SubTest> { public: void print() { // 直接子类是通行证的友元,可以合法构造实例调用方法 Test<SubTest>::print(PrintAccessKey{}); } }; // 间接子类 class SubSubTest: public SubTest { public: void print() { // 编译报错:SubSubTest不是PrintAccessKey的友元,无法构造通行证,不能调用Test::print // Test<SubTest>::print(Test<SubTest>::PrintAccessKey{}); } };
实际SSLsocket场景改造
对每一层需要限制跨层调用的类增加对应通行证,修改受限方法的签名即可:
// 改造SSLsocket类 template <typename T> class SSLsocket { protected: class SSLSendKey { friend T; // 仅直接子类T可构造 SSLSendKey() = default; }; // 受限的send方法,需要持有通行证才能调用 void send(char *buff, int size, SSLSendKey) { std::cout << "SSLsocket send" << std::endl; socket.send(buff, size); } private: friend Socket; void on_message(char *buff, int size) { std::cout << "SSLsocket on_message" << std::endl; reinterpret_cast<T *>(this)->on_message(buff, size); } Socket socket; }; // 改造websocket类 template <typename T> class websocket : public SSLsocket<websocket<T>> { protected: class WSSendKey { friend T; // 仅直接子类T可构造 WSSendKey() = default; }; public: // 受限的send方法,需要持有本层通行证才能调用 void send(char *buffer, int size, WSSendKey) { std::cout << "websocket send" << std::endl; // 作为SSLsocket的直接子类,可构造SSLSendKey调用父类send SSLsocket<websocket<T>>::send(buffer, size, SSLSendKey{}); } private: friend SSLsocket<websocket<T>>;; void on_message(char *buff, int size) { std::cout << "websocket on_message" << std::endl; reinterpret_cast<T *>(this)->on_message(buff, size); } }; // 改造最终类whatever class whatever : public websocket<whatever> { public: void send(char *buffer, int size) { std::cout << "whatever send" << std::endl; // 仅能调用直接父类websocket的send,可构造WSSendKey websocket<whatever>::send(buffer, size, WSSendKey{}); // 编译报错:whatever不是SSLSendKey的友元,无法构造通行证,不能跨层调用SSLsocket::send // SSLsocket<websocket<whatever>>::send(buffer, size, {}); } private: friend websocket<whatever>; void on_message(char *buff, int size) { std::cout << "whatever on_message" << std::endl; send(nullptr, 0); } }; // Socket类其余代码无需修改
方案优势
- 所有权限校验在编译期完成,无任何运行时开销
- 每层访问控制独立,可灵活配置哪些方法允许跨层调用、哪些仅对直接子类开放
- 不需要修改原有调用链路逻辑,侵入性极低
- 完全符合无虚函数、基于CRTP实现的要求
内容的提问来源于stack exchange,提问作者antho
相关产品推荐
相关产品推荐

