私人成员常驻访问者之间的比较

时间:2016-04-12 02:46:30

标签: c++ c++11 optimization standards

这个问题的主要部分是关于为类内部的私有数据成员创建公共只读访问器的正确且计算效率最高的方法。具体来说,使用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; }
};

“getter / 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.

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

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

    因此我的问题分为两部分。

    1. 这两种方法有何不同,是什么原因导致只有某些优化标志才会比其他方法更快/更慢?
    2. 为什么既定的做法是使用常量的“getter”函数,而使用常量引用很少知道,更不用说了?
    3. 正如评论所指出的那样,我的基准测试存在缺陷,与手头的问题无关。但是,对于上下文,它可以位于修订历史记录中。

4 个答案:

答案 0 :(得分:15)

问题#2的答案是,有时候,你可能想要改变类内部。如果您公开了所有属性,那么它们就会成为界面的一部分,所以即使您想出了一个不需要它们的更好的实现(比如,它可以快速重新计算价值并刮胡子)每个实例的大小使得其中1亿个程序现在使用的内存减少400-800 MB),您无法在不破坏相关代码的情况下将其删除。

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

答案 1 :(得分:12)

语义/行为差异远比您的(破碎)基准更重要。

复制语义已被破坏

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;
}

收率:

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而不是shortlonglong long

使用getter返回,您可以将内部表示切换为其他类型,甚至可以忽略该成员并动态计算。

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

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

对象尺寸增加

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

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

{x 1}}类在x86上的大小为Broken,在x86-64平台上的大小为8(后者因为填充,因为指针在8字节边界上对齐)。

增加的大小可以破坏CPU缓存,由于它可能很快会遇到大量项目的缓慢下降(好吧,并不是因为它已经破坏了16的向量很容易赋值运算符)。

更好的调试性能

只要getter的实现在类定义中是内联的,那么只要你通过足够的优化级别(Broken-O2进行编译,编译器就会剥离getter,{{ 1}}可能无法启用内联来保留堆栈跟踪。)

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

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

答案 2 :(得分:7)

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

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

答案 3 :(得分:4)

回答问题2:

const_cast<int&>(mcb.myIntegerAccessor) = 4;

将其隐藏在getter函数后面是一个很好的理由。这是一种聪明的方法来进行类似getter的操作,但它完全打破了类中的抽象。