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); // 初始化方法...
};
理解总结
- 这个类中列出了一些“属性”字段,比如
align
、string
、wrapWidth
等。 - 这些“属性”很直观地是类的数据成员,但实际项目中,想把这些直接字段换成属性(比如封装成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++目前没有原生支持属性语法,只能靠重载运算符模拟。
- 你若追求最高效率,可以用模板配合成员指针偏移和宿主类接口,消除动态调用。