“Practical Type Erasure” 中提到的 基于 boost::any
的配置框架,是一个典型的 类型擦除(Type Erasure) 应用案例。下面是详细解释,帮助你理解其作用和原理。
什么是 Type Erasure?
Type Erasure(类型擦除) 是一种技术,它允许你在不知道对象具体类型的情况下,依然能安全地存储、传递和操作这个对象。
使用场景:配置框架(Configuration Framework)
你想写一个通用配置系统,允许用户存储不同类型的配置项,比如:
config["port"] = 8080; // int
config["host"] = "localhost"; // std::string
config["debug"] = true; // bool
config["timeout"] = 3.5; // double
但这些值的类型都是不一样的,怎么办?
Boost.Any 的作用
boost::any
是 C++98/03 时期 Boost 库提供的类型擦除容器,可以在运行时安全地存储任何类型的值。
#include <boost/any.hpp>
#include <map>
#include <string>
std::map<std::string, boost::any> config;
config["port"] = 8080;
config["host"] = std::string("localhost");
config["debug"] = true;
存储任意类型的值 —— 类型擦除!
但你还可以这样取值:
int port = boost::any_cast<int>(config["port"]);
std::string host = boost::any_cast<std::string>(config["host"]);
如果类型不匹配,any_cast
会抛出 boost::bad_any_cast
异常。
典型实现:基于 boost::any
的配置系统
class Config {
public:
template<typename T>
void set(const std::string& key, const T& value) {
data_[key] = value; // 隐式转为 boost::any
}
template<typename T>
T get(const std::string& key) const {
return boost::any_cast<T>(data_.at(key));
}
private:
std::map<std::string, boost::any> data_;
};
使用:
Config config;
config.set("port", 8080);
config.set("title", std::string("My App"));
int port = config.get<int>("port");
std::string title = config.get<std::string>("title");
小结
项目 | 描述 |
---|---|
目标 | 存储任意类型的配置项 |
技术 | 类型擦除(Type Erasure) |
工具 | boost::any (或现代 C++ 的 std::any ) |
优点 | 灵活、不需要模板或继承 |
缺点 | 运行时开销大,类型错误在运行时才会发现 |
提示
- 在 C++17 中,建议使用
std::any
替代boost::any
,语义相同。 - 如果你想做类型安全或支持更多抽象操作,可以用
std::function
、std::variant
或自定义接口实现更复杂的 Type Erasure 框架。
你提到的这段代码展示了 Type Erasure(类型擦除) 的基本结构。下面是对这段代码的详细讲解和背后的思想分析:
你给出的代码:
class C {
public:
template<typename TInject>
C(TInject injectedInstance);
void invoke();
};
这段代码的核心概念:类型擦除
目标:类 C
能够接受任意类型的 injectedInstance
,但你又希望在 invoke()
中以统一的方式调用它的某些功能,不关心它具体是什么类型。
这就是典型的 “Type Erasure”:把不同类型的对象“隐藏”在一个统一接口背后。
如何实现?
这段代码只是框架。要真正实现类型擦除,通常会有以下结构:
完整实现步骤(简化版)
#include <memory>
#include <iostream>
// 1. 定义统一的接口
class ICallable {
public:
virtual void invoke() = 0;
virtual ~ICallable() = default;
};
// 2. 包装任意类型的对象
template<typename T>
class CallableImpl : public ICallable {
public:
CallableImpl(T obj) : obj_(obj) {}
void invoke() override {
obj_(); // 假设传进来的 T 是可调用的(函数/函数对象/lambda)
}
private:
T obj_;
};
// 3. 类型擦除类
class C {
public:
template<typename TInject>
C(TInject injectedInstance)
: impl_(std::make_shared<CallableImpl<TInject>>(injectedInstance)) {}
void invoke() {
impl_->invoke();
}
private:
std::shared_ptr<ICallable> impl_;
};
使用方式:
void hello() {
std::cout << "Hello, world!" << std::endl;
}
int main() {
C c1(hello); // 传入函数指针
C c2([]{ std::cout << "Lambda called!" << std::endl; }); // 传入 lambda
c1.invoke(); // 输出 Hello, world!
c2.invoke(); // 输出 Lambda called!
}
概念 | 说明 |
---|---|
Type Erasure | 编译期隐藏具体类型,仅暴露统一接口 |
目的 | 让类 C 接收任意类型,并通过统一方式操作它们 |
关键技术 | 接口类 + 模板包装器 + 智能指针 |
实际用途 | 类似于 std::function 、std::any 、策略模式 |
这段代码是一个 Type Erasure(类型擦除) 的实际实现,它让类 C
可以接受任意类型的对象,只要这些对象有 Deploy()
方法。下面是详细讲解:
你给出的结构精简整理后如下:
class C {
public:
template<typename TInject>
C(TInject injectedInstance)
: m_internalBasePtr(std::make_shared<CInjected<TInject>>(injectedInstance)) {}
void invoke() {
m_internalBasePtr->DoIt();
}
private:
struct CInternalBase {
virtual void DoIt() = 0;
virtual ~CInternalBase() {}
};
template<typename TInjected>
struct CInjected : public CInternalBase {
CInjected(TInjected i) : m_injected(i) {}
virtual void DoIt() override {
m_injected.Deploy(); // 要求 TInjected 有 Deploy() 方法
}
TInjected m_injected;
};
std::shared_ptr<CInternalBase> m_internalBasePtr;
};
这个实现做了什么?
1. 目的:
通过模板 + 虚函数,让类 C
能持有任意类型的对象,只要该对象提供 Deploy()
方法。
2. 关键组件解释
成员 | 作用 |
---|---|
CInternalBase | 抽象基类,定义了统一的接口 DoIt() 。 |
CInjected<T> | 模板派生类,用于包装任意类型 T ,并实现 DoIt() 。 |
m_internalBasePtr | 用于保存实际类型的对象,类型被擦除了。 |
C::invoke() | 调用 DoIt() ,触发实际对象的 Deploy() 方法。 |
示例用法
你可以这么用这个类:
struct MyType {
void Deploy() {
std::cout << "MyType deployed!" << std::endl;
}
};
int main() {
MyType obj;
C c(obj); // 类型被擦除
c.invoke(); // 调用的是 obj.Deploy()
}
输出:
MyType deployed!
总结:
知识点 | 内容 |
---|---|
Type Erasure | 使用模板+继承+虚函数,把具体类型“隐藏”在统一接口后面 |
优点 | 类 C 现在可以存储任意类型,只要它实现 Deploy() |
实现技巧 | 使用模板类继承接口类 + shared_ptr 管理生命周期 |
类似案例 | std::function , std::any , std::shared_ptr 的内部实现机制 |
下面是用 std::any
改写后的完整代码:
#include <vector>
#include <string>
#include <iostream>
#include <any> // 使用 C++17 的 std::any
int main()
{
std::any a = std::string("Anything?");
std::vector<std::string> v = {"Anything!"};
a = v;
a = 5; // 最终 a 中存储的是 int
std::cout << std::any_cast<int>(a) << std::endl;
return 0;
}
注意事项:
- 头文件改为
<any>
。 - 类型从
boost::any
改为std::any
。 std::any_cast<T>(a)
的用法和boost::any_cast
基本一致。- 要启用 C++17 标准 编译(否则编译器会报错):
编译方式示例(GCC 或 Clang):
g++ -std=c++17 your_file.cpp -o your_program
总结
std::any
是 C++17 引入的用于类型擦除的类型容器。- 能够存储任意类型的对象(复制构造可用的),并且通过
std::any_cast
进行类型安全的访问。 - 替代
boost::any
,可以减少对 Boost 的依赖。
Type Erasure(类型擦除) 在 C++ 中的实际应用场景。我们逐项来解释这些内容,帮助你理解这个概念的实际意义:
什么是 Type Erasure?
Type Erasure(类型擦除) 是一种设计技术,它允许你隐藏类型信息,以便不同类型的对象看起来像一个类型。你可以把它理解为一种让 C++ 支持运行时多态而不使用继承和虚函数表的方式。
实际应用场景(Practical Use Cases)
这些都是 Type Erasure 的经典应用:
1. Smart Pointer Deletion(智能指针删除器)
std::unique_ptr<Base, Deleter> ptr;
Deleter
是一个可以自定义的 Callable。std::unique_ptr
使用 类型擦除来存储Deleter
,它不在乎你用的是 lambda、函数指针,还是其他对象。- Type-erased deleter 允许
unique_ptr
支持不同的删除策略。
2. Heterogeneous Containers(异构容器)
std::vector<std::any> values;
- 容器里存放不同类型的元素(如
int
、std::string
等)。 std::any
实现了 类型擦除,把任意类型的对象封装起来,统一接口访问。
3. any_iterator(Type-Erased Iterator)
- 并非标准库,但很多库实现了
any_iterator
:- 可以统一不同类型容器的迭代器接口。
- 例如:你可以通过
any_iterator
遍历std::vector<int>
或std::list<int>
,代码不需要知道容器类型。
4. std::function(函数封装器)
std::function<void()> f = [] { std::cout << "Hi\n"; };
std::function
是最经典的类型擦除工具之一。- 它可以封装:
- 普通函数
- 成员函数
- lambda
- 函数对象(functor)
- 所有这些最终表现为同一种类型 ——
std::function<Signature>
。
总结
应用 | 使用场景 | 背后原理 |
---|---|---|
std::function | 封装任意 Callable | 类型擦除 + 虚调度 |
std::any | 存任意类型值 | 类型擦除 + 类型标识 |
智能指针 Deleter | 自定义删除行为 | 类型擦除 |
any_iterator | 通用迭代器抽象 | 类型擦除 |
这些机制允许 C++ 写出更灵活、更泛化、更现代化的代码。 |
Practical Type Erasure(实际类型擦除) 的一些关键注意点和潜在陷阱。下面我来逐条解释:
Type Erasure 并不是魔法(Not Magic)
虽然类型擦除看起来像魔法(你可以操作“未知类型”的对象),但实际上:
- 背后是复杂的模板、虚函数、多态和堆分配等机制。
- 最终必须有某一层知道具体类型,否则编译器没法生成正确代码。
Someone Must Know the Type(某个地方必须知道具体类型)
比如:
std::any a = 42;
std::cout << std::any_cast<int>(a); // 必须告诉它是 int
std::any_cast<int>
就是告诉编译器类型的地方。- 如果你猜错类型,程序会抛出异常或崩溃。
Polymorphism Possible, but Ugly(可以实现多态,但写法可能很丑)
C++ 本身有传统的多态(虚函数 + 继承),但你也可以用类型擦除实现“伪多态”:
class Drawable {
public:
template<typename T>
Drawable(T t) : self(std::make_shared<model<T>>(std::move(t))) {}
void draw() const { self->draw(); }
private:
struct concept {
virtual ~concept() = default;
virtual void draw() const = 0;
};
template<typename T>
struct model : concept {
T data;
model(T x) : data(std::move(x)) {}
void draw() const override { data.draw(); }
};
std::shared_ptr<const concept> self;
};
这种代码:
- 灵活、强大
- 但结构复杂、不直观
- 阅读和维护代价大
Use with Caution(谨慎使用)
类型擦除容易让代码:
- 变得晦涩难懂(比如你不知道变量实际是什么类型)
- 很难调试(错误信息模糊)
- 可维护性差(新同事可能完全读不懂)
使用场景:
非常合适:
- 插件系统、脚本语言接口
- 统一函数调用接口(如
std::function
) - 泛化配置系统(如使用
std::any
)
不推荐: - 普通业务逻辑中滥用
- 用来代替模板或继承时没有必要的情况
总结
点 | 含义 |
---|---|
Not Magic | 背后仍然依赖 C++ 的机制(虚函数、模板、堆分配) |
Someone Must Know | 最终必须知道类型,比如 any_cast 、类型模板参数 |
Polymorphism Ugly | 虽然能实现“接口”,但写法较丑,易出错 |
Use with Caution | 若滥用,可能导致难维护的代码、类型不安全、调试困难等问题 |
这段内容是在描述一个面向客户端的架构设计理念,特别是在构建一个可扩展、可插拔的数据访问或服务接口时的策略。下面来逐句解释这些要点:
Architecture 架构设计核心思想
Client-facing Front End Interface
面向客户端的前端接口
这是指你定义一个统一的接口层(比如DataProvider
、ServiceRegistry
等),客户端只通过这个接口与系统交互。
客户端不关心后端具体怎么实现,只管发请求、拿结果。
Return any object or data by key
根据键返回任意对象或数据
这说明你的接口是基于键值(key-based)访问的,比如:
auto result = service.get("user_info");
- 类似于动态配置、插件系统
- 背后可能是
std::any
、类型擦除、或工厂模式
Client decides on backend(s) to use
客户端决定使用哪些后端
这意味着:
- 后端是可插拔的
- 客户端可以选择使用哪个实现(甚至可以运行时选择)
比如:
Backend* b = BackendFactory::get("SQLite");
或在构建配置中指定:
--use_backend=Redis
Compile only used back ends
只编译被使用的后端
为了减小编译时间和可执行文件体积:
- 架构要允许按需编译
- 可以用插件机制或
#ifdef
控制编译某些 backends:
#ifdef USE_REDIS_BACKEND
#include "redis_backend.hpp"
#endif
Client ignorant of implementation
客户端对实现细节一无所知
这强调了**“解耦”**:
- 客户端只使用前端接口(如
get(key)
) - 客户端不需要知道使用的是 Redis、文件系统、数据库还是内存实现
- 这让系统更灵活、更容易维护和替换后端
总结一句话:
这种架构允许客户端只关注“我要什么”,而不是“怎么给我”,通过键访问抽象对象,并允许只编译和加载需要的后端实现。
后端(Back end)架构的设计要点,通常用来说明系统中后端模块的职责和特性。详细理解如下:
Architecture — Back end(架构设计 —— 后端)
Extendible(可扩展)
后端应具有良好的扩展性,能够支持新增功能或类型。
- 后端设计要支持动态增加新的功能模块或支持更多数据类型
- 例如支持新增数据库驱动、文件格式或消息协议时不需改动核心代码
Supports specified types(支持指定类型)
后端能处理系统规定的数据类型或对象类型。
- 明确后端处理的数据范围(如字符串、整数、结构体、复杂对象等)
- 例如数据库后端支持存储和查询的字段类型,文件后端支持的序列化格式等
Instances created and destroyed(支持实例的创建和销毁)
后端允许动态创建和销毁其实例。
- 后端并非单例,而是能同时存在多个独立实例
- 这些实例可以被创建、配置、销毁,支持生命周期管理
- 便于多任务或多用户场景,互不干扰
Multiple instances supported(支持多个实例)
同时允许存在多个后端实例。
- 比如一个程序可能同时连接多个数据库,或多个缓存系统实例
- 每个实例有自己独立状态和配置
- 支持负载均衡、分布式等高级用例
总结:
后端设计要做到灵活可扩展,能处理多种数据类型,支持多个实例动态管理,这保证了系统的模块化、复用性以及满足复杂场景需求。
class CAnyProperty
{
public:
// 智能指针类型,指向所有处理器基类,支持多态
typedef std::shared_ptr<CAnyHandlerBase> THandlerPtr;
// 模板方法,根据key获取指定类型T的值
template<typename T>
T Get(const std::string & key) const;
// 模板方法,根据key设置指定类型T的值
template<typename T>
void Set(const std::string & key, const T & value);
// 添加一个获取处理器,所有与其对应类型相关的Get调用会使用它
void AddGetHandler(THandlerPtr handler_ptr);
// 模板方法,为类型T设置对应的Set处理器
template<typename T>
void SetSetHandler(THandlerPtr handler_ptr);
private:
// 类型信息到获取处理器集合的映射,支持一个类型多个获取处理器
typedef std::map<Loki::TypeInfo, std::vector<THandlerPtr>> TGetHandlerMap;
TGetHandlerMap m_GetHandlerMap;
// 类型信息到设置处理器的映射,一个类型对应一个设置处理器
typedef std::map<Loki::TypeInfo, THandlerPtr> TSetHandlerMap;
TSetHandlerMap m_SetHandlerMap;
};
这段代码描述了一个面向客户端的通用属性访问接口(CAnyProperty
类),它通过“类型擦除”和“多态”机制,支持对任意类型的属性进行获取(Get)和设置(Set),并且可以注册不同类型的处理器(Handler)来定制具体的访问逻辑。
主要成员和设计思路
-
模板方法 Get 和 Set
template<typename T> T Get(const std::string &key) const
通过字符串key
获取对应的类型为T
的属性值。template<typename T> void Set(const std::string &key, const T &value)
通过key
设置类型为T
的属性值。
-
处理器 Handler(策略模式)
- 支持注册“获取”处理器:
AddGetHandler(THandlerPtr handler_ptr)
- 支持注册“设置”处理器:
template<typename T> void SetSetHandler(THandlerPtr handler_ptr)
- 处理器是指派给特定类型处理对应Get或Set逻辑的类,使用基类指针
CAnyHandlerBase
进行抽象。
- 支持注册“获取”处理器:
-
两个映射表维护不同类型的处理器
m_GetHandlerMap
:以类型信息(Loki::TypeInfo
)为键,存储一组对应的获取处理器。- 一个类型可能有多个获取处理器。
m_SetHandlerMap
:以类型信息为键,存储一个对应的设置处理器。- 一个类型仅对应一个设置处理器。
设计目的和用法
- 允许客户端通过统一的接口动态获取或设置不同类型的数据,而不必在编译期确定类型。
- 通过注册不同的“处理器”,可以灵活定义某个类型数据的获取和设置行为。
Loki::TypeInfo
用于运行时区分类型,实现类型安全的类型擦除。
总结
CAnyProperty
是一个客户端暴露的通用属性接口,支持基于类型擦除的多类型访问,并允许通过处理器机制定制不同类型属性的获取和设置逻辑,实现了灵活且可扩展的设计,满足“客户端只关注接口、背后多后端实现”的需求。
这个类是“后端处理器”的基类,提供统一的接口供前端调用,同时支持多态,方便不同具体后端实现自己的行为。
详细理解和注释如下:
class CAnyHandlerBase
{
public:
CAnyHandlerBase() {} // 默认构造函数
virtual ~CAnyHandlerBase() {} // 虚析构,支持多态正确销毁
// 虚函数,获取指定key对应的属性值,返回boost::any类型(可容纳任意类型)
// 默认实现是抛出异常,表示该后端不支持Get操作
virtual boost::any Get(const std::string & /*key*/) const
{
throw CAnyPropertyException(CAnyPropertyException::eNoGet);
return boost::any(); // 不会执行到这里
}
// 虚函数,设置指定key对应的属性值,参数为boost::any类型(任意类型)
// 默认实现是抛出异常,表示该后端不支持Set操作
virtual void Set(const std::string & key, const boost::any & /*value*/)
{
throw CAnyPropertyException(CAnyPropertyException::eNoSet);
}
// 纯虚函数,返回后端名称,用于错误报告和调试
virtual std::string Name() const = 0;
// 纯虚函数,返回该后端支持处理的类型列表(用Loki::TypeInfo表示)
virtual std::vector<Loki::TypeInfo> GetHandledTypes() const = 0;
};
核心点理解:
- 多态接口:前端通过基类指针或智能指针调用不同具体后端的Get/Set。
- 异常机制:默认实现抛异常,强制后端显式实现支持的操作。
- 类型信息:通过
GetHandledTypes()
告知前端哪些类型它可以处理。 - 后端名字:
Name()
方法用于诊断错误,方便定位问题。
这个 CAnyProperty
类是“客户端接口”,提供统一的 Get
和 Set
接口让客户端用键(key
)访问任意类型的数据。内部实际调用了私有的通用接口(比如 x_GetAny
和 x_SetAny
),把类型擦除到 boost::any
,实现对任意类型的支持。
详细理解:
class CAnyProperty
{
public:
// 获取指定key对应的值,模板参数T表示期望的类型
// 内部调用x_GetAny返回boost::any,再用boost::any_cast转换成T类型
template<typename T>
T Get(const std::string& key) const
{
return boost::any_cast<T>(x_GetAny(key, typeid(T)));
}
// 设置指定key对应的值,模板参数T表示传入值的类型
// 内部调用x_SetAny进行存储
template<typename T>
void Set(const std::string& key, const T& value)
{
x_SetAny(key, value);
}
private:
// x_GetAny 和 x_SetAny 是该类的私有成员函数,负责底层操作
// 这里没有给出定义,实际会调度到相应后端实现
boost::any x_GetAny(const std::string& key, const std::type_info& type) const;
void x_SetAny(const std::string& key, const boost::any& value);
};
核心点
- 模板接口:使用模板参数
T
支持任意类型的获取和设置,调用方便且类型安全。 - 类型擦除:底层用
boost::any
作为通用存储格式,把类型信息封装隐藏起来。 - 类型检查:
boost::any_cast
负责在运行时检查类型是否匹配,防止错误使用。 - 键值访问:通过字符串键索引数据,支持类似字典或属性集的访问方式。
- 底层细节隐藏:客户端代码无须知道数据是如何存储和管理的,只关注
Get
和Set
的类型接口。
这个 CAnyProperty::x_GetAny
函数是 “Glue Getter”(粘合器读取函数),它连接前端接口和后端实现,负责根据类型和键名查找对应的处理器(handler),从后端获取对应的数据并返回。
详细分析
boost::any
CAnyProperty::x_GetAny(const std::string& key, const Loki::TypeInfo& value_type) const
{
// 如果key为空,抛异常
if (key.empty()) throw CAnyPropertyException(CAnyPropertyException::eEmptyKey);
// 根据value_type(类型信息)去查找对应的“Get处理器列表”
TGetHandlerMap::const_iterator handler_list_iter = m_GetHandlerMap.find(value_type);
// 如果没有对应类型的处理器,抛异常,提示无读取处理器
if (m_GetHandlerMap.end() == handler_list_iter) {
throw CAnyPropertyException(CAnyPropertyException::eNoReadHandler, value_type.name());
}
// 取出该类型对应的处理器列表(可能多个)
const TGetHandlerMap::mapped_type& handler_list = handler_list_iter->second;
// 在处理器列表中执行条件搜索(比如按key匹配),找到对应的处理器
CQueryHandler a_query_handler =
for_each_if(handler_list.begin(), handler_list.end(), CQueryHandler(key));
// 如果没找到对应key,抛异常
if (a_query_handler.GetValue().empty()) {
throw CAnyPropertyNoKeyException(eKeyNotFound, key);
}
// 获取处理器查询到的值,类型是boost::any
boost::any a = a_query_handler.GetValue();
// 返回该值
return a;
}
关键点
-
参数说明
key
:字符串类型的键,用于查找对应数据value_type
:通过Loki::TypeInfo
表示的目标数据类型(类型信息)
-
m_GetHandlerMap
这是一个从类型信息映射到一组“读取处理器”(getter handlers)的映射表。每种类型可能对应多个处理器。 -
for_each_if
是一个自定义的算法,类似std::find_if
,在多个处理器中找到“能处理当前key”的那个。 -
CQueryHandler
是一个封装了查找和获取值的辅助类,用来匹配key并提取结果。 -
异常处理
代码里严格检查了各种异常情况:- 空key
- 类型无处理器
- key无对应值
-
返回值
返回一个boost::any
,包含了对应的值,保持了类型擦除,方便客户端通过boost::any_cast
转换成对应类型。
总结
这个函数是“桥梁”,负责从指定类型的所有getter处理器里,找到合适的处理器查询对应key的数据,统一返回结果。它实现了类型和键的双重查找逻辑,是整个“多后端多类型”属性系统的核心读操作。
这段代码展示了一个基于类型擦除的属性访问系统中,“读写操作”和“处理器注册”部分的核心机制。结合之前的CAnyProperty
类,可以理解为“前端接口”与“后端处理器”之间的桥梁代码。
1. for_each_if
函数模板
template<typename InputIterator, typename Function>
Function for_each_if(InputIterator first, InputIterator last, Function f)
{
for (; first != last; ++first)
if (f(*first))
return f;
break; // 注意这里实际应是 return f; 或者循环结束后的return,break写法不正确
}
- 功能:遍历
[first, last)
范围内的元素,调用f(*first)
,如果返回true
,则立即返回函数对象f
。 - 这是自定义的条件遍历器,用于查找满足某条件的第一个元素,并返回执行状态的函数对象。
2. CQueryHandler
类 — 读操作谓词
class CQueryHandler : public std::unary_function<CAnyProperty::THandlerPtr, bool>
{
public:
CQueryHandler(const std::string& key) : m_Key(key) {}
boost::any GetValue() const { return m_Value; }
// 调用operator(),用handler_ptr去尝试读取key对应的值
bool operator()(CAnyProperty::THandlerPtr handler_ptr)
{
assert(m_Value.empty());
m_Value = handler_ptr->Get(m_Key);
return !m_Value.empty();
}
private:
std::string m_Key;
boost::any m_Value;
};
- 这是一个函数对象,用于对每个
handler_ptr
调用Get(key)
,尝试读取数据。 - 如果读到了非空数据(即存在有效值),返回
true
,否则false
。 - 成功读取的数据会保存到
m_Value
,供后续访问。
3. x_SetAny
函数 — 写操作桥接函数
void CAnyProperty::x_SetAny(const std::string& key, const boost::any& value)
{
if (key.empty()) {
throw CAnyPropertyException(CAnyPropertyException::eEmptyKey);
}
Loki::TypeInfo value_type(value.type());
TSetHandlerMap::iterator handler_iter = m_SetHandlerMap.find(value_type);
if (handler_iter == m_SetHandlerMap.end()) {
throw CAnyPropertyException(CAnyPropertyException::eNoWriteHandler, value_type.name());
}
THandlerPtr handler_ptr = handler_iter->second;
assert(handler_ptr);
handler_ptr->Set(key, value);
}
- 根据传入的
value
的类型,从写处理器映射表中找到对应的写处理器。 - 找不到写处理器会抛异常。
- 找到后调用对应写处理器的
Set(key, value)
完成写操作。
4. 添加处理器相关接口
inline void CAnyProperty::AddGetHandler(CAnyProperty::THandlerPtr handler_ptr)
{
std::vector<Loki::TypeInfo> handled_types = handler_ptr->GetHandledTypes();
for (auto type_iter : handled_types) {
TGetHandlerMap::mapped_type& handler_list = m_GetHandlerMap[type_iter];
handler_list.push_back(handler_ptr);
}
}
template<typename T>
void CAnyProperty::SetSetHandler(THandlerPtr handler_ptr)
{
m_SetHandlerMap[Loki::TypeInfo(typeid(T))] = handler_ptr;
}
AddGetHandler
:把一个读处理器注册到对应的多个类型映射里,因为一个处理器可能支持多种类型。SetSetHandler
:给某个具体类型注册写处理器,写处理器一对一绑定类型。
总结
- 读操作:通过类型找到对应的多个读处理器,遍历调用
Get(key)
,找到第一个能返回非空值的处理器并返回结果。 - 写操作:根据值类型找到唯一写处理器,调用
Set(key, value)
完成写入。 - 处理器管理:支持动态注册读写处理器,形成扩展性强的插件式架构。
- 设计目的:实现面向客户端的灵活接口,背后可以支持多种类型和多种数据后端,解耦类型与具体实现,方便扩展和维护。
这段代码展示了 后端处理器(Backend Handlers) 的两个实现例子,配合前面你提到的基类CAnyHandlerBase
,说明了如何实现不同类型和不同存储介质的“读写”逻辑。
1. CAnyHandlerBase
(基类)
- 职责:定义接口,后端处理器都继承它,实现具体的
Get
、Set
、类型支持等方法。 Get
、Set
默认抛异常,说明子类必须覆盖,否则不支持对应操作。Name()
用于返回处理器名称(调试、错误信息用)。GetHandledTypes()
返回此处理器支持的类型列表。
2. CAnyPropertyHandlerMemory<TValue>
(简单内存存储处理器)
这是一个模板类,支持任意类型TValue
,通过内部std::map<std::string, TValue>
来存储键值对。
template <typename TValue>
class CAnyPropertyHandlerMemory : public CAnyHandlerBase
{
public:
virtual boost::any Get(const std::string& key) const override
{
boost::any value;
auto it = m_Map.find(key);
if (it != m_Map.end()) {
value = it->second;
}
return value;
}
virtual void Set(const std::string& key, const boost::any& value) override
{
m_Map[key] = boost::any_cast<TValue>(value);
}
virtual std::vector<Loki::TypeInfo> GetHandledTypes() const override
{
return CreateTypeVector<TValue>()();
}
private:
std::map<std::string, TValue> m_Map;
};
-
功能:
Get
:查找m_Map
,返回对应的值(boost::any
包装)。Set
:将boost::any
转换成TValue
存入m_Map
。
-
适用场景:内存临时存储,适合简单、快速的数据保存。
-
CreateTypeVector<TValue>()()
是辅助函数,用于返回TValue
的类型信息列表,告诉系统该处理器能处理哪些类型。
3. CAnyHandlerEnv
(环境变量处理器)
boost::any CAnyHandlerEnv::Get(const std::string& key) const
{
boost::any value;
char* env_value = ::getenv(key.c_str());
if (env_value) {
value = std::string(env_value);
}
return value;
}
void CAnyHandlerEnv::Set(const std::string& key, const boost::any& value)
{
std::string env_value = key + "=" + boost::any_cast<std::string>(value);
int putenvReturn = ::putenv(const_cast<char*>(env_value.c_str()));
if (putenvReturn) {
throw CAnyPropertyException(...);
}
}
std::vector<Loki::TypeInfo> CAnyHandlerEnv::GetHandledTypes() const
{
return CreateTypeVector<std::string>()();
}
- 功能:
- 通过系统API访问环境变量,
Get
时使用getenv
,返回字符串环境变量值。 Set
时,调用putenv
设置环境变量,传入格式是"key=value"
。
- 通过系统API访问环境变量,
- 类型:仅支持
std::string
类型的读写。 - 异常处理:
putenv
失败时抛异常,保证调用者能感知错误。
总结
- 这两个类是
CAnyHandlerBase
的具体实现,分别演示了:- 内存存储后端(任意类型支持,基于模板实现)
- 环境变量后端(只支持字符串类型)
- 它们共同体现了后端的职责:
- 提供具体数据存取逻辑
- 声明自己支持的类型
- 与前端
CAnyProperty
结合,可以实现统一接口,背后多后端切换,灵活且扩展性强。
这段内容展示了两个更复杂的 后端处理器(Backend Handlers) 示例,扩展了前面的简单内存和环境变量处理器,说明了如何处理复杂数据结构和实际业务需求。
1. 后端 JSON 处理器 — CAnyHandlerJSON
- 场景:处理 JSON 格式数据,将 JSON 文件中的内容加载进内存,方便统一访问和操作。
- 结构示例(JSON):
{
"firstName": "Homer",
"lastName": "Simpson",
"age": 38,
"address": {
"streetAddress": "742 Evergreen Terrace",
"city": "Springfield",
"state": "OR",
"postalCode": "96522"
},
"phoneNumber": [
{ "type": "home", "number": "939 555-1234" },
{ "type": "fax", "number": "636 555-4567" }
]
}
- 实现思路:
class CAnyHandlerJSON : public CAnyHandlerBase
{
// 存储键值对,值用 boost::any 支持多种类型
std::map<std::string, boost::any> m_values;
public:
CAnyHandlerJSON(const std::string& jsonFileName)
{
// 解析 JSON 文件,假设 topObject 是解析后的根 JSON 对象
m_values["firstName"] = topObject["firstName"].get_value<std::string>();
m_values["lastName"] = topObject["lastName"].get_value<std::string>();
m_values["age"] = topObject["age"].get_value<int>();
// 对嵌套的地址字段,先转换成 map<string,string>
std::map<std::string, std::string> addressMap;
// 解析 address 子对象,赋值到 addressMap
// ...
m_values["address"] = addressMap;
// 对电话字段,转换成包含结构体(如 SPhoneNumber)的 vector
std::vector<SPhoneNumber> phoneVector;
// 解析 phoneNumber 数组,填充 phoneVector
// ...
m_values["phone"] = phoneVector;
}
// 其他必要接口(Get、Set、Name、GetHandledTypes)未展示
};
-
特点:
- 支持复杂数据结构(嵌套对象、数组)
- 内部用
boost::any
可存储多种类型,包括自定义结构体、容器等 - 适合配置文件、数据交换格式的解析和访问
2. 真实业务后端示例 — CGPAttrHandlerBuildrunID
- 场景:与数据库交互,获取或设置具体业务数据(例如构建运行ID)
- 类设计:
class IConnection; // 抽象数据库连接接口
class CGPAttrHandlerBuildrunID : public CGPAttrHandlerBase
{
public:
virtual boost::any Get(const std::string& key) const override;
virtual std::string Name() const override;
virtual std::vector<Loki::TypeInfo> GetHandledTypes() const override;
CGPAttrHandlerBuildrunID();
private:
void x_ConnectToDatabase();
int x_GetBuildID() const;
std::string x_ConstructSQL() const;
CGPipeProperty m_Environment; // 可能是环境配置或上下文对象
std::string m_Database;
std::string m_Username;
std::string m_Password;
std::auto_ptr<IConnection> m_Connection; // 负责数据库连接生命周期管理
};
- 设计要点:
- 实现数据库连接、SQL构造、数据获取的私有函数
- 通过
Get
函数执行查询返回数据(包装成boost::any
) - 使用智能指针管理数据库连接资源
- 支持多个后端类型和复杂逻辑(可能异步、事务等)
总结
- JSON Handler 用于支持结构化、层次化数据的存取,适合配置文件或数据交换。
- 数据库 Handler 体现了“后端”灵活可扩展的特性,可以封装复杂的业务逻辑和资源管理。
- 这两个例子都继承了相同基类,实现了统一接口,方便前端透明访问不同数据源。
- 设计思想体现“解耦”“统一接口”“多态扩展”。
“Practical Type Erasure”(实用类型擦除)设计的核心要点:
1. No Magic Bullet – Someone will have to cast
“没有万能灵药——总得有人来做类型转换。”
虽然类型擦除技术让我们可以在接口层隐藏具体类型,实现更灵活的多态,但底层还是要进行类型转换(cast)。这需要程序员明确知道什么时候以及如何进行安全的转换,避免类型错误。
2. Helps Expose Clean Interfaces – Even when internals are dirty
“即使内部复杂,也能暴露干净的接口。”
类型擦除允许封装复杂且多样的实现细节,前端接口保持简洁统一,用户无需关心内部如何实现。它提升了模块化和接口设计的清晰度。
3. Glues OO and Generic Code
“将面向对象和泛型代码粘合在一起。”
类型擦除是连接传统面向对象编程(多态、虚函数等)和现代泛型编程(模板、类型参数化等)的桥梁。它让你用统一的方式处理各种不同类型的对象,而无需在代码中显式指定所有类型。
总体理解
类型擦除不是魔法,也不会自动解决所有问题,但它是构建灵活、模块化、高度抽象系统的重要技术。它帮助你:
- 提供整洁的API和接口;
- 隐藏内部复杂细节;
- 结合面向对象和模板泛型的优势。
不过,需要谨慎使用,确保类型转换安全和代码可维护。