The main portion of this question is in regards to the proper and most computationally efficient method of creating a public read-only accessor for a private data member inside of a class. Specifically, utilizing a const type &
reference to access the variables such as:
这个问题的主要部分是关于为类内部的私有数据成员创建公共只读访问器的正确且计算效率最高的方法。具体来说,利用const类型和引用来访问变量,例如:
class MyClassReference
{
private:
int myPrivateInteger;
public:
const int & myIntegerAccessor;
// Assign myPrivateInteger to the constant accessor.
MyClassReference() : myIntegerAccessor(myPrivateInteger) {}
};
However, the current established method for solving this problem is to utilize a constant "getter" function as seen below:
然而,目前建立的解决该问题的方法是利用恒定的“getter”函数,如下所示:
class MyClassGetter
{
private:
int myPrivateInteger;
public:
int getMyInteger() const { return myPrivateInteger; }
};
The necessity (or lack thereof) for "getters/setters" has already been hashed out time and again on questions such as: Conventions for accessor methods (getters and setters) in C++ That however is not the issue at hand.
“getter / setters”的必要性(或缺乏)已经在诸如以下问题上反复出现:C ++中访问方法(getter和setter)的约定然而这不是问题。
Both of these methods offer the same functionality using the syntax:
这两种方法都使用以下语法提供相同的功能:
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.
After discovering this, and finding nothing on the internet concerning it, I asked several of my mentors and professors for which is appropriate and what are the relative advantages/disadvantages of each. However, all responses I received fell nicely into two categories:
在发现这一点,并在互联网上找不到任何关于它的内容之后,我问了几位适当的导师和教授,以及每个导师和教授的相对优势/劣势。但是,我收到的所有回复都很好地分为两类:
- I have never even thought of that, but use a "getter" method as it is "Established Practice".
- They function the same (They both run with the same efficiency), but use a "getter" method as it is "Established Practice".
我从来没有想过这个,但是使用“getter”方法,因为它是“已建立的实践”。
它们的功能相同(它们都以相同的效率运行),但使用“getter”方法,因为它是“已建立的实践”。
While both of these answers were reasonable, as they both failed to explain the "why" I was left unsatisfied and decided to investigate this issue further. While I conducted several tests such as average character usage (they are roughly the same), average typing time (again roughly the same), one test showed an extreme discrepancy between these two methods. This was a run-time test for calling the accessor, and assigning it to an integer. Without any -OX
flags (In debug mode), the MyClassReference
performed roughly 15% faster. However, once a -OX
flag was added, in addition to performing much faster both methods ran with the same efficiency.
虽然这两个答案都是合理的,因为他们都没有解释“为什么”我不满意,并决定进一步调查这个问题。虽然我进行了几项测试,例如平均字符使用(它们大致相同),平均打字时间(再次大致相同),但一项测试表明这两种方法之间存在极端差异。这是一个用于调用访问器并将其分配给整数的运行时测试。没有任何-OX标志(在调试模式下),MyClassReference的执行速度大约快15%。但是,一旦添加了-OX标志,除了执行速度更快之外,两种方法都以相同的效率运行。
My question is thus has two parts.
因此,我的问题有两个部分。
- How do these two methods differ, and what causes one to be faster/slower than the others only with certain optimization flags?
- Why is it that established practice is to use a constant "getter" function, while using a constant reference is rarely known let alone utilized?
这两种方法有何不同,是什么原因导致只有某些优化标志才会比其他方法更快/更慢?
为什么既定的做法是使用恒定的“getter”函数,而使用常量引用很少知道,更不用说了?
As comments pointed out, my benchmark testing was flawed, and irrelevant to the matter at hand. However, for context it can be located in the revision history.
正如评论所指出的那样,我的基准测试存在缺陷,与手头的问题无关。但是,对于上下文,它可以位于修订历史记录中。
4 个解决方案
#1
12
There are semantic/behavioral differences that are far more significant than your (broken) benchmarks.
语义/行为差异远比您的(破碎)基准更重要。
Copy semantics are broken
复制语义被破坏了
A 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;
}
Yields:
5 42
The problem is that when doing a copy, copy.read_only
points to original.read_write
. This may lead to dangling references (and crashes).
问题是,在复制时,copy.read_only指向original.read_write。这可能会导致悬空引用(和崩溃)。
This can be fixed by writing your own copy constructor, but it is painful.
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
Assignment is broken
作业被打破了
A reference cannot be reseated (you can alter the content of its referee but not switch it to another referee), 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;
}
generating an error:
生成错误:
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
This can be fixed by writing your own copy constructor, but it is painful.
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
Unless you fix it, Broken
can only be used in very restricted ways; you may never manage to put it inside a std::vector
for example.
除非你修复它,否则只能以非常有限的方式使用Broken;你可能永远不会把它放在std :: vector中。
Increased coupling
Giving away a reference to your internals increases coupling. You leak an implementation detail (the fact that you are using an int
and not a short
, long
or long long
).
放弃对内部的引用会增加耦合。泄漏实现细节(事实上您使用的是int而不是short,long或long long)。
With a getter returning a value, you can switch the internal representation to another type, or even elide the member and compute it on the fly.
使用getter返回值,您可以将内部表示切换为另一种类型,甚至可以忽略该成员并动态计算它。
This is only significant if the interface is exposed to clients expecting binary/source-level compatibility; if the class is only used internally and you can afford to change all users if it changes, then this is not an issue.
只有当接口暴露给期望二进制/源级兼容性的客户端时,这才有意义;如果该类仅在内部使用,并且您可以承担更改所有用户的费用,那么这不是问题。
Now that semantics are out of the way, we can speak about performance differences.
现在语义已经不在了,我们可以谈论性能差异。
Increased object size
对象大小增加
While references can sometimes be elided, it is unlikely to ever happen here. This means that each reference member will increase the size of an object by at least sizeof(void*)
, plus potentially some padding for alignment.
虽然有时可以省略引用,但这里不太可能发生。这意味着每个引用成员将增加对象的大小至少sizeof(void *),以及可能的一些填充用于对齐。
The original class MyClassA
has a size of 4
on x86 or x86-64 platforms with mainstream compilers.
原始类MyClassA在x86或x86-64平台上的大小为4,主流编译器。
The Broken
class has a size of 8
on x86 and 16
on x86-64 platforms (the latter because of padding, as pointers are aligned on 8-bytes boundaries).
Broken类在x86上的大小为8,在x86-64平台上的大小为16(后者由于填充,因为指针在8字节边界上对齐)。
An increased size can bust up CPU caches, with a large number of items you may quickly experience slow downs due to it (well, not that it'll be easy to have vectors of Broken
due to its broken assignment operator).
增加的大小可以破坏CPU缓存,由于它可能很快就会遇到减速的大量项目(好吧,并不是因为它的分配操作符被破坏而很容易出现Broken的向量)。
Better performance in debug
调试性能更好
As long as the implementation of the getter is inline in the class definition, then the compiler will strip the getter whenever you compile with a sufficient level of optimizations (-O2
or -O3
generally, -O1
may not enable inlining to preserve stack traces).
只要getter的实现在类定义中内联,那么编译器将在您使用足够的优化级别进行编译时剥离getter(-O2或-O3通常,-O1可能无法启用内联以保留堆栈跟踪) 。
Thus, the performance of access should only vary in debug code, where performance is least necessary (and otherwise so crippled by plenty of other factors that it matters little).
因此,访问性能应该只在调试代码中有所不同,在调试代码中,性能是最不必要的(否则会受到许多其他重要因素的影响)。
In the end, use a getter. It's established convention for a good number of reasons :)
最后,使用一个吸气剂。它的成立惯例有很多原因:)
#2
15
The answer to question #2 is that sometimes, you might want to change class internals. If you made all your attributes public, they're part of the interface, so even if you come up with a better implementation that doesn't need them (say, it can recompute the value on the fly quickly and shave the size of each instance so programs that make 100 million of them now use 400-800 MB less memory), you can't remove it without breaking dependent code.
问题#2的答案是,有时,您可能想要更改类内部。如果你公开了所有的属性,那么它们就是接口的一部分,所以即使你想出了一个不需要它们的更好的实现(比如,它可以快速重新计算这个值,并削减每个属性的大小)实例所以使1亿个程序现在使用400-800 MB内存的程序),你不能在不破坏依赖代码的情况下删除它。
With optimization turned on, the getter function should be indistinguishable from direct member access when the code for the getter is just a direct member access anyway. But if you ever want to change how the value is derived to remove the member variable and compute the value on the fly, you can change the getter implementation without changing the public interface (a recompile would fix up existing code using the API without code changes on their end), because a function isn't limited in the way a variable is.
启用优化后,当getter的代码只是直接成员访问时,getter函数应该与直接成员访问无法区分。但是,如果您想要更改值的派生方式以删除成员变量并动态计算值,则可以在不更改公共接口的情况下更改getter实现(重新编译将使用API修复现有代码而无需更改代码在他们的结尾),因为函数不受变量的限制。
#3
7
When implementing constant reference (or constant pointer) your object also stores a pointer, which makes it bigger in size. Accessor methods, on the other hand, are instantiated only once in program and are most likely optimized out (inlined), unless they are virtual or part of exported interface.
当实现常量引用(或常量指针)时,对象还存储指针,这使得它的大小更大。另一方面,访问器方法仅在程序中实例化一次,并且最有可能优化(内联),除非它们是虚拟的或者是导出接口的一部分。
By the way, getter method can also be virtual.
顺便说一句,getter方法也可以是虚拟的。
#4
4
To answer question 2:
回答问题2:
const_cast<int&>(mcb.myIntegerAccessor) = 4;
Is a pretty good reason to hide it behind a getter function. It is a clever way to do a getter-like operation, but it completely breaks abstraction in the class.
将其隐藏在getter函数后面是一个很好的理由。这是一种聪明的方法来进行类似getter的操作,但它完全打破了类中的抽象。
#1
12
There are semantic/behavioral differences that are far more significant than your (broken) benchmarks.
语义/行为差异远比您的(破碎)基准更重要。
Copy semantics are broken
复制语义被破坏了
A 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;
}
Yields:
5 42
The problem is that when doing a copy, copy.read_only
points to original.read_write
. This may lead to dangling references (and crashes).
问题是,在复制时,copy.read_only指向original.read_write。这可能会导致悬空引用(和崩溃)。
This can be fixed by writing your own copy constructor, but it is painful.
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
Assignment is broken
作业被打破了
A reference cannot be reseated (you can alter the content of its referee but not switch it to another referee), 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;
}
generating an error:
生成错误:
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
This can be fixed by writing your own copy constructor, but it is painful.
这可以通过编写自己的复制构造函数来解决,但这很痛苦。
Unless you fix it, Broken
can only be used in very restricted ways; you may never manage to put it inside a std::vector
for example.
除非你修复它,否则只能以非常有限的方式使用Broken;你可能永远不会把它放在std :: vector中。
Increased coupling
Giving away a reference to your internals increases coupling. You leak an implementation detail (the fact that you are using an int
and not a short
, long
or long long
).
放弃对内部的引用会增加耦合。泄漏实现细节(事实上您使用的是int而不是short,long或long long)。
With a getter returning a value, you can switch the internal representation to another type, or even elide the member and compute it on the fly.
使用getter返回值,您可以将内部表示切换为另一种类型,甚至可以忽略该成员并动态计算它。
This is only significant if the interface is exposed to clients expecting binary/source-level compatibility; if the class is only used internally and you can afford to change all users if it changes, then this is not an issue.
只有当接口暴露给期望二进制/源级兼容性的客户端时,这才有意义;如果该类仅在内部使用,并且您可以承担更改所有用户的费用,那么这不是问题。
Now that semantics are out of the way, we can speak about performance differences.
现在语义已经不在了,我们可以谈论性能差异。
Increased object size
对象大小增加
While references can sometimes be elided, it is unlikely to ever happen here. This means that each reference member will increase the size of an object by at least sizeof(void*)
, plus potentially some padding for alignment.
虽然有时可以省略引用,但这里不太可能发生。这意味着每个引用成员将增加对象的大小至少sizeof(void *),以及可能的一些填充用于对齐。
The original class MyClassA
has a size of 4
on x86 or x86-64 platforms with mainstream compilers.
原始类MyClassA在x86或x86-64平台上的大小为4,主流编译器。
The Broken
class has a size of 8
on x86 and 16
on x86-64 platforms (the latter because of padding, as pointers are aligned on 8-bytes boundaries).
Broken类在x86上的大小为8,在x86-64平台上的大小为16(后者由于填充,因为指针在8字节边界上对齐)。
An increased size can bust up CPU caches, with a large number of items you may quickly experience slow downs due to it (well, not that it'll be easy to have vectors of Broken
due to its broken assignment operator).
增加的大小可以破坏CPU缓存,由于它可能很快就会遇到减速的大量项目(好吧,并不是因为它的分配操作符被破坏而很容易出现Broken的向量)。
Better performance in debug
调试性能更好
As long as the implementation of the getter is inline in the class definition, then the compiler will strip the getter whenever you compile with a sufficient level of optimizations (-O2
or -O3
generally, -O1
may not enable inlining to preserve stack traces).
只要getter的实现在类定义中内联,那么编译器将在您使用足够的优化级别进行编译时剥离getter(-O2或-O3通常,-O1可能无法启用内联以保留堆栈跟踪) 。
Thus, the performance of access should only vary in debug code, where performance is least necessary (and otherwise so crippled by plenty of other factors that it matters little).
因此,访问性能应该只在调试代码中有所不同,在调试代码中,性能是最不必要的(否则会受到许多其他重要因素的影响)。
In the end, use a getter. It's established convention for a good number of reasons :)
最后,使用一个吸气剂。它的成立惯例有很多原因:)
#2
15
The answer to question #2 is that sometimes, you might want to change class internals. If you made all your attributes public, they're part of the interface, so even if you come up with a better implementation that doesn't need them (say, it can recompute the value on the fly quickly and shave the size of each instance so programs that make 100 million of them now use 400-800 MB less memory), you can't remove it without breaking dependent code.
问题#2的答案是,有时,您可能想要更改类内部。如果你公开了所有的属性,那么它们就是接口的一部分,所以即使你想出了一个不需要它们的更好的实现(比如,它可以快速重新计算这个值,并削减每个属性的大小)实例所以使1亿个程序现在使用400-800 MB内存的程序),你不能在不破坏依赖代码的情况下删除它。
With optimization turned on, the getter function should be indistinguishable from direct member access when the code for the getter is just a direct member access anyway. But if you ever want to change how the value is derived to remove the member variable and compute the value on the fly, you can change the getter implementation without changing the public interface (a recompile would fix up existing code using the API without code changes on their end), because a function isn't limited in the way a variable is.
启用优化后,当getter的代码只是直接成员访问时,getter函数应该与直接成员访问无法区分。但是,如果您想要更改值的派生方式以删除成员变量并动态计算值,则可以在不更改公共接口的情况下更改getter实现(重新编译将使用API修复现有代码而无需更改代码在他们的结尾),因为函数不受变量的限制。
#3
7
When implementing constant reference (or constant pointer) your object also stores a pointer, which makes it bigger in size. Accessor methods, on the other hand, are instantiated only once in program and are most likely optimized out (inlined), unless they are virtual or part of exported interface.
当实现常量引用(或常量指针)时,对象还存储指针,这使得它的大小更大。另一方面,访问器方法仅在程序中实例化一次,并且最有可能优化(内联),除非它们是虚拟的或者是导出接口的一部分。
By the way, getter method can also be virtual.
顺便说一句,getter方法也可以是虚拟的。
#4
4
To answer question 2:
回答问题2:
const_cast<int&>(mcb.myIntegerAccessor) = 4;
Is a pretty good reason to hide it behind a getter function. It is a clever way to do a getter-like operation, but it completely breaks abstraction in the class.
将其隐藏在getter函数后面是一个很好的理由。这是一种聪明的方法来进行类似getter的操作,但它完全打破了类中的抽象。