当前位置: 首页 > news >正文

CppCon 2015 学习:Implementing class properties effectively

这段内容讲的是C++中“属性”(Property)的实现及其设计理念,并结合一个实际类Text来说明。中文理解如下:

关于“属性”(Property)

  • 属性:介于类的字段(field)和方法(method)之间的东西。
    也就是说,属性看起来像成员变量,但可以通过方法(getter/setter)控制访问和赋值,实现更灵活的封装和控制。

代码示例类 Text

class Text : public BaseElement
{
public:ZOBJ(Text);                   // 可能是宏定义,做一些元编程的辅助Text() : string(nullptr) { }  // 构造函数,初始化 string 为 nullptrint align;                    // 对齐方式,属性示例之一ZString *string;              // 字符串指针int stringLength;             // 字符串长度float drawOffsetY;            // 绘制时的Y轴偏移FontGeneric *font;            // 字体对象指针float wrapWidth;              // 自动换行宽度DynamicArray *formattedStrings; // 格式化字符串数组DynamicArray *multiDrawers;   // 多重绘制器float maxHeight;              // 最大高度bool wrapLongWords;           // 是否允许长词换行virtual Text *initWithFont(FontGeneric *i);  // 初始化方法...
};

理解总结

  • 这个类中列出了一些“属性”字段,比如alignstringwrapWidth等。
  • 这些“属性”很直观地是类的数据成员,但实际项目中,想把这些直接字段换成属性(比如封装成getter/setter)可以增强安全性和灵活性。
  • 这正是“属性”的本质:外观是数据成员,但实际上是通过方法控制访问的接口。
  • 你可以用类似C#的属性语法来模拟,或者用宏/模板等方式来简化写法。

这段代码体现了大量遗留代码(legacy code)的实际用法和风格,中文理解如下:

代码分析

Text* text;
...
text->setText(ZS(STR_LOC_OMNOM));
...
text->width = 42;
text->height = GetQuadOffset(IMG_OMNOM__top_offs).y;
...
text->draw();

理解总结

  • text 是指向 Text 类实例的指针。
  • text->setText(ZS(STR_LOC_OMNOM));
    这是通过方法(setter)设置文本内容,ZS(STR_LOC_OMNOM) 可能是某种字符串宏或转换函数。
  • text->width = 42;text->height = ...;
    直接通过公有成员变量访问和赋值宽高。
    这里是“直接访问字段”的风格,缺乏封装性和安全性。
  • text->draw();
    调用对象的方法进行绘制。

说明

  • 这种混合用法:部分用方法访问(setText()),部分直接访问字段(width, height)是典型的遗留代码风格。
  • 直接字段访问容易导致难以控制属性的变化,也难以插入额外逻辑(如验证、事件通知等)。
  • 这也是为什么现代代码倾向于将“属性”封装为 getter/setter 或类似机制。

1. 需求背景

想模拟C#里“属性”(Property)的语法:

foo.size = 10; // 实际调用setter
int x = foo.size; // 实际调用getter

而不是直接访问成员变量。

2. 最简单的实现——成员变量直接暴露

class Bar {
public:int size;
};

用法:

Bar bar;
bar.size = 10;  // 直接赋值成员变量

缺点:不能拦截赋值或读取操作,无法封装额外逻辑(例如校验、触发事件等)。

3. 用getter/setter封装访问

class Foo {
private:int size;
public:void setSize(const int& s) { size = s; }const int& getSize() const { return size; }
};

用法:

Foo foo;
foo.setSize(10);       // 设置
int x = foo.getSize(); // 读取

缺点:调用写法不够简洁。

4. Property模板类模拟属性语法糖

想实现:

foo.size = 10;  // 自动调用setSize
int x = foo.size; // 自动调用getSize

这需要通过C++运算符重载和隐式类型转换来实现。

5. 纯存储型Property(不满足调用宿主方法需求)

template<typename T>
class Property {
private:T value;
public:Property<T>& operator=(const T& v) {value = v;return *this;}operator const T&() {return value;}
};
  • 赋值重载=operator=,赋值给内部成员
  • 隐式转换operator T&(),读取时返回内部成员引用
    缺点
  • 不能调用宿主对象的方法,不能实现额外逻辑。

6. 用std::function封装getter/setter,满足调用宿主函数需求

template<typename T>
class Property {
private:std::function<void(const T&)> setter;std::function<const T&()> getter;
public:Property(std::function<void(const T&)> s, std::function<const T&()> g): setter(s), getter(g) {}Property<T>& operator=(const T& v) {setter(v);return *this;}operator const T&() {return getter();}
};

用法示例:

class Test {
private:float dimension;
public:const float& getArea() const {// 假设返回引用可能不安全,这里简化示例static float area = dimension * dimension;return area;}void setArea(const float& val) {dimension = std::sqrt(val);}Property<float> area;Test() : area([this](const float& v){ setArea(v); },[this]() -> const float& { return getArea(); }) {}
};
  • Property对象内部保存两个std::function,指向宿主对象的方法。
  • 赋值时调用setter,读取时调用getter。

缺点

  • std::function内部存储函数对象(闭包),开销较大,至少有堆分配和类型擦除,sizeof(Property)通常较大(例如32字节)。
  • 每次调用通过std::function间接,性能下降。

7. 用成员函数指针(member function pointers)优化

成员函数指针示例:

struct Test {void setVal(const int& v) { /* ... */ }const int& getVal() const { /* ... */ return val; }int val;
};
  • 成员函数指针类型:
    • void (Test::*)(const int&) ——指向成员函数void setVal(const int&)
    • const int& (Test::*)() const ——指向成员函数const int& getVal() const

8. 结合成员函数指针写Property模板

template <typename T, typename Host>
class Property {
private:void (Host::*setter)(const T&);const T& (Host::*getter)() const;Host* host;
public:Property(void (Host::*set)(const T&), const T& (Host::*get)() const, Host* h): setter(set), getter(get), host(h) {}Property<T, Host>& operator=(const T& value) {(host->*setter)(value);  // 调用宿主setterreturn *this;}operator const T&() const {return (host->*getter)(); // 调用宿主getter}
};

优点:

  • Property对象只存储3个指针(setter、getter成员函数指针 + 指向宿主实例的指针),通常24字节。
  • std::function轻量许多。
  • 调用时通过成员函数指针调用宿主函数,调用开销更低。

缺点:

  • 调用时仍有两次间接调用:
    • host->*setter,调用成员函数指针;
    • 成员函数调用本身也有一定调用开销(尤其虚函数)。

9. 进一步优化思考:利用成员指针偏移

成员指针本质上是宿主类对象中某成员的偏移(对数据成员而言),或成员函数的地址。
如果只操作数据成员(不是调用函数),我们可以将“成员指针”当做偏移量,在编译期知道偏移量,直接用指针+偏移访问成员数据。
比如:

template<typename T, typename Host, T Host::*member>
class Property {
private:Host* host;
public:Property(Host* h) : host(h) {}Property<T, Host, member>& operator=(const T& value) {host->*member = value;  // 直接访问成员变量return *this;}operator const T&() const {return host->*member;   // 直接访问成员变量}
};

10. 将“调用宿主方法”的逻辑加入

如果你想要“赋值时调用宿主的setter函数”,读取时调用getter函数,而不直接访问成员变量,需要你在Host类中写统一的接口,比如:

struct Foo {int size_;void setSize(const int& v) { size_ = v; }const int& getSize() const { return size_; }
};

配合:

template<typename T, typename Host, T Host::*member>
class Property {
private:Host* host;
public:Property(Host* h) : host(h) {}Property<T, Host, member>& operator=(const T& value) {host->setSize(value);  // 这里硬编码了setSize,不能通用return *this;}operator const T&() const {return host->getSize();}
};

但这显然不通用,也不能自动匹配getter/setter,除非借助宏或者模板技巧(如成员函数指针传参)。

11. 性能对比总结

方案内存大小(示例)调用层数优点缺点
纯成员变量sizeof(T)0简单快速不能调用getter/setter
Property + std::function~32字节3灵活,可任意函数封装大开销,调用慢
Property + 成员函数指针~24字节2较轻量,调用宿主函数调用间接层依旧存在
Property + 成员变量指针(偏移)~8字节1轻量,调用快只能直接访问数据成员

12. 实际应用示例

struct Foo {int size_;void setSize(const int& v) { size_ = v; }const int& getSize() const { return size_; }
};
template<typename T, typename Host>
class Property {
private:void (Host::*setter)(const T&);const T& (Host::*getter)() const;Host* host;
public:Property(void (Host::*set)(const T&), const T& (Host::*get)() const, Host* h): setter(set), getter(get), host(h) {}Property<T, Host>& operator=(const T& value) {(host->*setter)(value);return *this;}operator const T&() const {return (host->*getter)();}
};
int main() {Foo foo;Property<int, Foo> sizeProp(&Foo::setSize, &Foo::getSize, &foo);sizeProp = 10;               // 调用foo.setSize(10)std::cout << int(sizeProp); // 调用foo.getSize()
}

13. 总结

  • 模拟属性的最佳方案取决于效率与灵活性的权衡:
    • 最高效的是直接数据成员指针操作,但无法封装逻辑。
    • 最灵活的是std::function,但内存占用和运行时开销大。
    • 成员函数指针折中方案较常用,性能和灵活性都较好。
  • C++目前没有原生支持属性语法,只能靠重载运算符模拟。
  • 你若追求最高效率,可以用模板配合成员指针偏移和宿主类接口,消除动态调用。

相关文章:

  • RocketMQ延迟消息机制
  • 【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
  • 第5章 类的基本概念 笔记
  • 不变性(Immutability)模式
  • b2b企业网络营销如何用deepseek、豆包等AI平台获客 上海添力
  • switch选择语句
  • 打造多模态交互新范式|彩讯股份中标2025年中国移动和留言平台AI智能体研发项目
  • Linux内核 -- INIT_WORK 使用与注意事项
  • Win系统下的Linux系统——WSL 使用手册
  • 如何根据excel表生成sql的insert脚本
  • [ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
  • PyArk飘云阁出品的ARK工具
  • IP地址可视化:从现网监控到合规检测、准入控制全面管理
  • Microsoft Azure 马来西亚区域正式上线
  • 大模型原理、架构与落地
  • 黑马python(三)
  • Css实现悬浮对角线边框动效
  • 智慧医疗能源事业线深度画像分析(上)
  • leetcode题解450:删除BST中的结点!调整二叉树的结构最难!
  • DiffBP: generative diffusion of 3D molecules for target protein binding
  • 郴州市第四人民医院/网站seo优化方案策划书
  • 简述网站内容管理流程/宁波seo关键词排名
  • 手机怎么样做网站/seo外推
  • wordpress图像描述/南宁seo手段
  • 手机网站制作代理商/seo优化的主要内容
  • 网站建设哪个公司好知乎/开封网站推广公司