私有成员的常量访问者之间的比较
Comparison between constant accessors of private members
这个问题的主要部分是关于为 class 内部的私有数据成员创建 public 只读访问器的正确且计算效率最高的方法。具体来说,利用 const type &
引用访问变量,例如:
class MyClassReference
{
private:
int myPrivateInteger;
public:
const int & myIntegerAccessor;
// Assign myPrivateInteger to the constant accessor.
MyClassReference() : myIntegerAccessor(myPrivateInteger) {}
};
然而,目前解决这个问题的既定方法是利用常量 "getter" 函数,如下所示:
class MyClassGetter
{
private:
int myPrivateInteger;
public:
int getMyInteger() const { return myPrivateInteger; }
};
"getters/setters" 的必要性(或缺乏必要性)已经在诸如以下的问题上反复讨论过:Conventions for accessor methods (getters and setters) in C++ 然而,这不是当前的问题。
这两种方法使用语法提供相同的功能:
MyClassGetter a;
MyClassReference b;
int SomeValue = 5;
int A_i = a.getMyInteger(); // Allowed.
a.getMyInteger() = SomeValue; // Not allowed.
int B_i = b.myIntegerAccessor; // Allowed.
b.myIntegerAccessor = SomeValue; // Not allowed.
发现这一点后,在互联网上找不到任何相关信息,我问了我的几位导师和教授,哪个合适,每个人的亲戚是什么advantages/disadvantages。但是,我收到的所有回复都很好地分为两类:
- 我从来没有想过,但是使用 "getter" 方法,因为它是 "Established Practice"。
- 它们的功能相同(它们 运行 具有相同的效率),但使用 "getter" 方法,因为它是 "Established Practice".
虽然这两个答案都是合理的,但由于它们都未能解释 "why" 我感到不满意并决定进一步调查此问题。虽然我进行了多项测试,例如平均字符使用(它们大致相同)、平均打字时间(同样大致相同),但一项测试显示这两种方法之间存在极大差异。这是调用访问器并将其分配给整数的 运行 时间测试。没有任何 -OX
标志(在调试模式下),MyClassReference
的执行速度大约快 15%。但是,一旦添加了 -OX
标志,除了执行速度更快之外,这两种方法 运行 具有相同的效率。
因此我的问题分为两部分。
- 这两种方法有何不同,是什么导致一种方法 faster/slower 比其他方法仅具有某些优化标志?
- 为什么惯例是使用常量 "getter" 函数,而使用常量引用却鲜为人知,更不用说使用了?
正如评论所指出的,我的基准测试存在缺陷,与手头的事情无关。但是,对于上下文,它可以位于修订历史记录中。
回答问题 2:
const_cast<int&>(mcb.myIntegerAccessor) = 4;
是将它隐藏在 getter 函数后面的一个很好的理由。这是一种执行类似 getter 操作的聪明方法,但它完全破坏了 class.
中的抽象
问题 2 的答案是,有时您可能想要更改 class 内部结构。如果您创建了所有属性 public,它们就是接口的一部分,所以即使您想出一个不需要它们的更好的实现(例如,它可以快速重新计算值并剃须每个实例的大小,因此产生 1 亿个实例的程序现在使用的内存减少了 400-800 MB),您不能在不破坏相关代码的情况下删除它。
启用优化后,当 getter 的代码只是直接成员访问时,getter 函数应该与直接成员访问没有区别。但是如果你想更改值的派生方式以删除成员变量并动态计算值,你可以更改 getter 实现而不更改 public 接口(重新编译将修复使用 API 的现有代码,最后没有代码更改),因为函数不受变量限制。
当实现常量引用(或常量指针)时,您的对象还存储了一个指针,这使得它的大小更大。另一方面,访问器方法仅在程序中实例化一次并且很可能被优化掉(内联),除非它们是虚拟的或导出接口的一部分。
对了,getter方法也可以虚
有 semantic/behavioral 比您的(损坏的)基准测试更显着的差异。
复制语义被破坏
一个live example:
#include <iostream>
class Broken {
public:
Broken(int i): read_only(read_write), read_write(i) {}
int const& read_only;
void set(int i) { read_write = i; }
private:
int read_write;
};
int main() {
Broken original(5);
Broken copy(original);
std::cout << copy.read_only << "\n";
original.set(42);
std::cout << copy.read_only << "\n";
return 0;
}
产量:
5
42
问题是在复制的时候,copy.read_only
指向original.read_write
。这可能会导致 悬空引用 (和崩溃)。
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
分配被破坏
一个引用不能被重新定位(你可以改变它的引用的内容,但不能切换到另一个引用),leading to:
int main() {
Broken original(5);
Broken copy(4);
copy = original;
std::cout << copy.read_only << "\n";
original.set(42);
std::cout << copy.read_only << "\n";
return 0;
}
生成错误:
prog.cpp: In function 'int main()':
prog.cpp:18:7: error: use of deleted function 'Broken& Broken::operator=(const Broken&)'
copy = original;
^
prog.cpp:3:7: note: 'Broken& Broken::operator=(const Broken&)' is implicitly deleted because the default definition would be ill-formed:
class Broken {
^
prog.cpp:3:7: error: non-static reference member 'const int& Broken::read_only', can't use default assignment operator
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
除非您修复它,否则 Broken
只能以非常有限的方式使用;例如,您可能永远无法将它放在 std::vector
中。
增加耦合
放弃对内部结构的引用会增加耦合。您泄露了一个实现细节(您使用的是 int
而不是 short
、long
或 long long
)。
使用 getter 返回 value,您可以将内部表示切换为另一种类型,甚至可以删除成员并即时计算它。
只有当接口暴露给期望 binary/source-level 兼容性的客户端时,这才有意义;如果 class 仅在内部使用,并且您可以负担得起更改所有用户的费用,那么这不是问题。
既然语义已经不在话下,我们可以谈谈性能差异了。
增加对象大小
虽然引用有时会被省略,但这种情况永远不会发生。这意味着每个引用成员都会将对象的大小增加至少 sizeof(void*)
,加上可能的一些对齐填充。
原来的classMyClassA
在x86或x86-64平台主流编译器上大小为4
Broken
class 在 x86 平台上的大小为 8
,在 x86-64 平台上的大小为 16
(后者是因为填充,因为指针在8 字节边界)。
增加的大小会破坏 CPU 缓存,如果有大量项目,您可能会很快遇到速度变慢的情况(好吧,并不是说 [=16 的向量会很容易) =] 由于其损坏的赋值运算符)。
更好的调试性能
只要 getter 的实现在 class 定义中是内联的,那么只要您使用足够的优化级别进行编译,编译器就会去除 getter (-O2
或 -O3
通常,-O1
可能无法启用内联以保留堆栈跟踪)。
因此,访问的性能应该只在调试代码中有所不同,其中性能是最不需要的(否则会受到许多其他因素的影响,因此无关紧要)。
最后,用一个getter。出于多种原因,它已成为惯例 :)
这个问题的主要部分是关于为 class 内部的私有数据成员创建 public 只读访问器的正确且计算效率最高的方法。具体来说,利用 const type &
引用访问变量,例如:
class MyClassReference
{
private:
int myPrivateInteger;
public:
const int & myIntegerAccessor;
// Assign myPrivateInteger to the constant accessor.
MyClassReference() : myIntegerAccessor(myPrivateInteger) {}
};
然而,目前解决这个问题的既定方法是利用常量 "getter" 函数,如下所示:
class MyClassGetter
{
private:
int myPrivateInteger;
public:
int getMyInteger() const { return myPrivateInteger; }
};
"getters/setters" 的必要性(或缺乏必要性)已经在诸如以下的问题上反复讨论过:Conventions for accessor methods (getters and setters) in C++ 然而,这不是当前的问题。
这两种方法使用语法提供相同的功能:
MyClassGetter a;
MyClassReference b;
int SomeValue = 5;
int A_i = a.getMyInteger(); // Allowed.
a.getMyInteger() = SomeValue; // Not allowed.
int B_i = b.myIntegerAccessor; // Allowed.
b.myIntegerAccessor = SomeValue; // Not allowed.
发现这一点后,在互联网上找不到任何相关信息,我问了我的几位导师和教授,哪个合适,每个人的亲戚是什么advantages/disadvantages。但是,我收到的所有回复都很好地分为两类:
- 我从来没有想过,但是使用 "getter" 方法,因为它是 "Established Practice"。
- 它们的功能相同(它们 运行 具有相同的效率),但使用 "getter" 方法,因为它是 "Established Practice".
虽然这两个答案都是合理的,但由于它们都未能解释 "why" 我感到不满意并决定进一步调查此问题。虽然我进行了多项测试,例如平均字符使用(它们大致相同)、平均打字时间(同样大致相同),但一项测试显示这两种方法之间存在极大差异。这是调用访问器并将其分配给整数的 运行 时间测试。没有任何 -OX
标志(在调试模式下),MyClassReference
的执行速度大约快 15%。但是,一旦添加了 -OX
标志,除了执行速度更快之外,这两种方法 运行 具有相同的效率。
因此我的问题分为两部分。
- 这两种方法有何不同,是什么导致一种方法 faster/slower 比其他方法仅具有某些优化标志?
- 为什么惯例是使用常量 "getter" 函数,而使用常量引用却鲜为人知,更不用说使用了?
正如评论所指出的,我的基准测试存在缺陷,与手头的事情无关。但是,对于上下文,它可以位于修订历史记录中。
回答问题 2:
const_cast<int&>(mcb.myIntegerAccessor) = 4;
是将它隐藏在 getter 函数后面的一个很好的理由。这是一种执行类似 getter 操作的聪明方法,但它完全破坏了 class.
中的抽象问题 2 的答案是,有时您可能想要更改 class 内部结构。如果您创建了所有属性 public,它们就是接口的一部分,所以即使您想出一个不需要它们的更好的实现(例如,它可以快速重新计算值并剃须每个实例的大小,因此产生 1 亿个实例的程序现在使用的内存减少了 400-800 MB),您不能在不破坏相关代码的情况下删除它。
启用优化后,当 getter 的代码只是直接成员访问时,getter 函数应该与直接成员访问没有区别。但是如果你想更改值的派生方式以删除成员变量并动态计算值,你可以更改 getter 实现而不更改 public 接口(重新编译将修复使用 API 的现有代码,最后没有代码更改),因为函数不受变量限制。
当实现常量引用(或常量指针)时,您的对象还存储了一个指针,这使得它的大小更大。另一方面,访问器方法仅在程序中实例化一次并且很可能被优化掉(内联),除非它们是虚拟的或导出接口的一部分。
对了,getter方法也可以虚
有 semantic/behavioral 比您的(损坏的)基准测试更显着的差异。
复制语义被破坏
一个live example:
#include <iostream>
class Broken {
public:
Broken(int i): read_only(read_write), read_write(i) {}
int const& read_only;
void set(int i) { read_write = i; }
private:
int read_write;
};
int main() {
Broken original(5);
Broken copy(original);
std::cout << copy.read_only << "\n";
original.set(42);
std::cout << copy.read_only << "\n";
return 0;
}
产量:
5 42
问题是在复制的时候,copy.read_only
指向original.read_write
。这可能会导致 悬空引用 (和崩溃)。
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
分配被破坏
一个引用不能被重新定位(你可以改变它的引用的内容,但不能切换到另一个引用),leading to:
int main() {
Broken original(5);
Broken copy(4);
copy = original;
std::cout << copy.read_only << "\n";
original.set(42);
std::cout << copy.read_only << "\n";
return 0;
}
生成错误:
prog.cpp: In function 'int main()': prog.cpp:18:7: error: use of deleted function 'Broken& Broken::operator=(const Broken&)' copy = original; ^ prog.cpp:3:7: note: 'Broken& Broken::operator=(const Broken&)' is implicitly deleted because the default definition would be ill-formed: class Broken { ^ prog.cpp:3:7: error: non-static reference member 'const int& Broken::read_only', can't use default assignment operator
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
除非您修复它,否则 Broken
只能以非常有限的方式使用;例如,您可能永远无法将它放在 std::vector
中。
增加耦合
放弃对内部结构的引用会增加耦合。您泄露了一个实现细节(您使用的是 int
而不是 short
、long
或 long long
)。
使用 getter 返回 value,您可以将内部表示切换为另一种类型,甚至可以删除成员并即时计算它。
只有当接口暴露给期望 binary/source-level 兼容性的客户端时,这才有意义;如果 class 仅在内部使用,并且您可以负担得起更改所有用户的费用,那么这不是问题。
既然语义已经不在话下,我们可以谈谈性能差异了。
增加对象大小
虽然引用有时会被省略,但这种情况永远不会发生。这意味着每个引用成员都会将对象的大小增加至少 sizeof(void*)
,加上可能的一些对齐填充。
原来的classMyClassA
在x86或x86-64平台主流编译器上大小为4
Broken
class 在 x86 平台上的大小为 8
,在 x86-64 平台上的大小为 16
(后者是因为填充,因为指针在8 字节边界)。
增加的大小会破坏 CPU 缓存,如果有大量项目,您可能会很快遇到速度变慢的情况(好吧,并不是说 [=16 的向量会很容易) =] 由于其损坏的赋值运算符)。
更好的调试性能
只要 getter 的实现在 class 定义中是内联的,那么只要您使用足够的优化级别进行编译,编译器就会去除 getter (-O2
或 -O3
通常,-O1
可能无法启用内联以保留堆栈跟踪)。
因此,访问的性能应该只在调试代码中有所不同,其中性能是最不需要的(否则会受到许多其他因素的影响,因此无关紧要)。
最后,用一个getter。出于多种原因,它已成为惯例 :)