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

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++目前没有原生支持属性语法,只能靠重载运算符模拟。
  • 你若追求最高效率,可以用模板配合成员指针偏移和宿主类接口,消除动态调用。
http://www.xdnf.cn/news/13269.html

相关文章:

  • 维度建模是什么意思?如何实现维度建模?
  • Postgresql数据库初体验
  • 全连接网络
  • java常量池和字符串常量池
  • 24-Oracle 23 ai ​Lock-Free Reservations​(无锁列值保留)
  • Vue3通过自定义指令实现数字滚动动画效果
  • 《Playwright:微软的自动化测试工具详解》
  • 联邦学习聚合参数操作详解
  • 关于个性化头像框设计的分享与服务说明
  • cv::Range的用法
  • AI时代的“数据之困”,什么是AI-Ready Data
  • 介绍一种直流过压保护电路
  • 蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
  • AUTOSAR图解==>AUTOSAR_TR_SWCModelingGuide
  • 【Java工程师面试全攻略】Day7:分布式系统设计面试精要
  • C++ 类继承
  • 《驭码CodeRider 2.0深度体验:AI驱动研发全流程革新,开发效率飙升300%!》
  • 实现建筑互联互通目标,楼宇自控系统在设备管理中作用凸显
  • 如何通过DNS解析实现负载均衡?有哪些优势?
  • DICOM批量修改工具
  • Pytest断言全解析:掌握测试验证的核心艺术
  • 15、企业固定资产(FA)全流程解析:从资产购置到资产处置
  • 产品经理入门到精通:01需求调研
  • 【Pandas】pandas DataFrame isna
  • 详解pytorch
  • 【学习笔记】虚函数+虚析构函数
  • 半导体设备基本通信标准介绍
  • shell脚本拔高习题
  • Word-- 制作论文三线表
  • SQL SERVER 数据库迁移的三种方法!