Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Comparison between constant accessors of private members

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:

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:

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.

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:

  1. I have never even thought of that, but use a "getter" method as it is "Established Practice".
  2. They function the same (They both run with the same efficiency), but use a "getter" method as it is "Established Practice".

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.

My question is thus has two parts.

  1. How do these two methods differ, and what causes one to be faster/slower than the others only with certain optimization flags?
  2. Why is it that established practice is to use a constant "getter" function, while using a constant reference is rarely known let alone utilized?

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.

like image 433
Chris Britt Avatar asked Apr 12 '16 02:04

Chris Britt


People also ask

Should accessors be const?

Professors hammered it into my head when I was in school, associates have jumped down my throat for it on code reviews, and it's in pretty much every C++ textbook out there: "accessor" (aka "selector" or "getter") methods must be marked const . If it doesn't change or mutate the data, then mark it const .

What are mutators and accessors give examples?

In Java accessors are used to get the value of a private field and mutators are used to set the value of a private field. Accessors are also known as getters and mutators are also known as setters.

What is the difference between an accessor and a mutator method for a class?

An accessor is a class method used to read data members, while a mutator is a class method used to change data members. It's best practice to make data members private (as in the example above) and only access them via accessors and mutators.

Which of the following is a characteristic of an accessor?

Accessor Function They are used instead of making a class member variable public and changing it directly within an object. To access a private object member, an accessor function must be called. Typically for a member such as Level, a function GetLevel() returns the value of Level and SetLevel() to assign it a value.


3 Answers

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.

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.

like image 99
ShadowRanger Avatar answered Oct 23 '22 23:10

ShadowRanger


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).

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.

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).

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.

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.

The original class MyClassA has a size of 4 on x86 or x86-64 platforms with mainstream compilers.

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).

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).

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).

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 :)

like image 31
Matthieu M. Avatar answered Oct 23 '22 23:10

Matthieu M.


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.

like image 7
Andrei R. Avatar answered Oct 23 '22 23:10

Andrei R.