私人成员的持续访问者之间的比较

Chr*_*itt 19 c++ optimization standards c++11

这个问题的主要部分是关于为类内私有数据成员创建公共只读访问器的正确且计算效率最高的方法.具体来说,利用const type &引用来访问变量,例如:

class MyClassReference
{
private:
    int myPrivateInteger;

public:
    const int & myIntegerAccessor;

    // Assign myPrivateInteger to the constant accessor.
    MyClassReference() : myIntegerAccessor(myPrivateInteger) {}
};
Run Code Online (Sandbox Code Playgroud)

然而,目前建立的解决这个问题的方法是利用恒定的"getter"函数,如下所示:

class MyClassGetter
{
private:
    int myPrivateInteger;

public:
    int getMyInteger() const { return myPrivateInteger; }
};
Run Code Online (Sandbox Code Playgroud)

"getter/setters"的必要性(或缺乏)已经在诸如以下问题上反复出现:C++中访问方法(getter和setter)的约定然而这不是问题.

这两种方法都使用以下语法提供相同的功能:

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.
Run Code Online (Sandbox Code Playgroud)

在发现这一点,并在互联网上找不到任何相关内容之后,我问了几位适当的导师和教授,以及每个导师和教授的相对优势/劣势.但是,我收到的所有回复都很好地分为两类:

  1. 我从来没有想过这一点,但使用"getter"方法,因为它是"已建立的实践".
  2. 它们的功能相同(它们都以相同的效率运行),但使用"getter"方法,因为它是"Established Practice".

虽然这两个答案都是合理的,因为他们都没有解释"为什么"我不满意,并决定进一步调查这个问题.虽然我进行了几项测试,例如平均字符使用(它们大致相同),平均打字时间(同样大致相同),但一项测试表明这两种方法之间存在极端差异.这是一个运行时测试,用于调用访问器,并将其分配给整数.没有任何-OX标志(在调试模式下),MyClassReference执行速度大约快15%.但是,一旦-OX添加了标志,除了执行速度更快之外,两种方法都以相同的效率运行.

因此我的问题有两个部分.

  1. 这两种方法有何不同,是什么原因导致只有某些优化标志才能比其他方法更快/更慢?
  2. 为什么既定的做法是使用恒定的"getter"函数,而使用常量引用很少知道,更不用说了?

正如评论所指出的那样,我的基准测试存在缺陷,与手头的问题无关.但是,对于上下文,它可以位于修订历史中.

Sha*_*ger 15

问题#2的答案是,有时,您可能想要更改类内部.如果你公开了所有的属性,那么它们就是接口的一部分,所以即使你想出了一个不需要它们的更好的实现(比如,它可以快速重新计算值,并削减每个属性的大小)实例所以程序使其中1亿个现在使用400-800 MB的内存),你不能在不破坏依赖代码的情况下删除它.

启用优化后,当getter的代码只是直接成员访问时,getter函数应该与直接成员访问无法区分.但是,如果您想要更改值的派生方式以删除成员变量并动态计算值,则可以在不更改公共接口的情况下更改getter实现(重新编译将使用API​​修复现有代码而无需更改代码在他们的结尾),因为函数不受变量的限制.


Mat*_* M. 12

有语义/行为是比你(碎)基准更为显著差异.

复制语义被破坏了

一个实例:

#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;
}
Run Code Online (Sandbox Code Playgroud)

产量:

5
42
Run Code Online (Sandbox Code Playgroud)

问题是,在复制时,copy.read_only指向original.read_write.这可能会导致悬空引用(和崩溃).

这可以通过编写自己的复制构造函数来解决,但这很痛苦.

作业被打破了

无法重新引用引用(您可以更改其裁判的内容但不能将其切换到另一个裁判),从而导致:

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;
}
Run Code Online (Sandbox Code Playgroud)

生成错误:

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
Run Code Online (Sandbox Code Playgroud)

这可以通过编写自己的复制构造函数来解决,但这很痛苦.

除非你修理它,否则Broken只能以非常有限的方式使用; 你可能永远不会把它放在一个std::vector例子里面.

增加耦合

提供对内部的引用会增加耦合.您泄漏的实现细节(事实上,你正在使用的int,而不是short,long或long long).

使用getter返回值,您可以将内部表示切换为另一种类型,甚至可以忽略该成员并动态计算它.

只有当接口暴露给期望二进制/源级兼容性的客户端时,这才有意义; 如果该类仅在内部使用,并且您可以承担更改所有用户的费用,那么这不是问题.


现在语义已经不在了,我们可以谈论性能差异.

增加对象大小

虽然有时可以省略引用,但这里不太可能发生.这意味着每个引用成员将至少增加对象的大小sizeof(void*),加上可能的一些填充用于对齐.

原始类MyClassA的大小4在x86或x86-64平台上,主流编译器.

所述Broken类的大小为8在x86和16上X86-64平台(后者是因为填充的,因为指针上的8个字节的边界上对齐).

增加的大小可以破坏CPU缓存,由于它可能很快就会遇到减速的大量项目(好吧,并不是因为它的Broken分配操作符被破坏而很容易产生向量).

调试性能更好

只要getter的实现在类定义中是内联的,那么只要您使用足够的优化级别进行编译,编译器就会剥离getter(-O2或者-O3通常,-O1可能不会启用内联来保留堆栈跟踪).

因此,访问性能应该只在调试代码中有所不同,在调试代码中,性能是最不必要的(否则会受到许多其他重要因素的影响).


最后,使用一个吸气剂.它的成立惯例有很多原因:)


And*_* R. 7

当实现常量引用(或常量指针)时,对象还存储指针,这使得它的大小更大.另一方面,访问器方法仅在程序中实例化一次,并且最有可能优化(内联),除非它们是虚拟的或是导出接口的一部分.

顺便说一句,getter方法也可以是虚拟的.

  • 此外,默认的复制构造函数被危险地破坏,并且不再生成默认赋值运算符. (4认同)