19

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.

Community
  • 1
  • 1
Lilith Daemon
  • 1,473
  • 1
  • 19
  • 37
  • 7
    `g++ -std=c++11 -o test.bin main.cpp` - talking about performance of unoptimised code is normally a waste of time, as anyone who cares about performance will enable `-O2` or higher. I would expect an unoptimised function call to be slower. Separately, storing a reference or pointer tends to waste memory in the object, and is undesirable on that front. – Tony Delroy Apr 12 '16 at 02:53
  • 4
    I hate to be frank, but if your options really are `g++ -std=c++11 -o test.bin main.cpp`. Then you forgot to turn on optimizations and your benchmark is therefore useless. – Mysticial Apr 12 '16 at 02:53
  • @TonyD That is true, however the main issue with using any of the -OX arguments is they remove the redundant line `int a = ` and thus invalidating the entire test. – Lilith Daemon Apr 12 '16 at 02:54
  • 1
    @ChrisBritt: and that's why it'd have been better to ask *"how can I benchmark this meaningfully"*... normally you want to stash a random value into the object at run-time, then do something such as add the values during iteration. Getting values from the keyboard, or even using `argc` a la `int x = argc < 100 ? 2 : -1;` can yield values that can't be optimised away at compile time. – Tony Delroy Apr 12 '16 at 02:56
  • 8
    That's like saying, "Usain Bolt is so fast that I can't see when he crosses the line. Therefore I'm just gonna tell him to run slower." No, turning off optimizations is not the solution. You need to modify the benchmark in a way that prevents the compiler from removing unused code. – Mysticial Apr 12 '16 at 02:56
  • @Mysticial This is much more to highlight an edge-case than a real world optimization. (In reality, these calls are taking a little more than a nano second each. ) However, it still has the property of being an edge case where logic appears to be violated where certain accessors are shown to have fairly dramatic changes in operation time. (On a VERY large scale.) – Lilith Daemon Apr 12 '16 at 02:56
  • @Mysticial How would I best modify this to create a meaningful benchmark? I must confess this is not an area of expertise of mine. If instead of creating a new int each time, if I kept a persistent integer and increased its count by the stored value would the avoid compiler time optimization? – Lilith Daemon Apr 12 '16 at 02:59
  • 5
    @ChrisBritt: you might think the performance is only of academic interest here, but I can assure you that if say Standard Library containers' `empty()` calls were *any* slower than necessary, it'd have been sorted out years ago. Using accessor functions does not necessitate a performance compromise. – Tony Delroy Apr 12 '16 at 02:59
  • In the general case, you need to use the result of the computation to prevent the compiler from removing it. But in your case, what you're benchmarking is to small that it's almost meaningless. Compiler-optimizations are context sensitive. Benchmarking something so small in one context is not going to be very useful. – Mysticial Apr 12 '16 at 03:01
  • @Mystical By adding some additional computations (Summing a persistent a) and then returning and printing this value to the user I managed to get it working roughly the same using the `-O2` compiler flag. However, while it certainly is expected that having optimization will decrease the actual computation time, what specifically does it remove to make it so all three methods take roughly the same amount of time? (Even if it though it is faster, it seems logical that they still will keep the relative ratios of their computing time.) – Lilith Daemon Apr 12 '16 at 03:17
  • 3
    The compiler is probably inlining the getter and accessing the member directly. Likewise, it's probably skipping the reference indirection and accessing the member directly since it sees that's what you're trying to do. So in the end, you're benchmarking the same thing because the compiler sees that you're trying to do the same thing in both cases. – Mysticial Apr 12 '16 at 03:27
  • @Mysticial I suppose that makes sense. Thank you greatly to everyone who assisted me, and to everyone who took the time to read through my question. – Lilith Daemon Apr 12 '16 at 03:32
  • I expect the optimizer to have problems seeing through the reference access if the class instance isn't local (e.g., in a function that takes a `MyClassB` by const reference). Note also that as written the copy constructor of `MyClassB` is badly broken. – T.C. Apr 12 '16 at 08:27
  • You doubled or tripled the `sizeof` your class for no reason – M.M Apr 12 '16 at 09:00
  • 2
    @M.M: quadrupled the size in the test I just did. 64-bit architecture, pointers are 8-aligned, and the compiler hasn't re-ordered the data members. So `sizeof(MyClassA)` is just 4 for the int, and `sizeof(MyClassB)` is 16: 4 for the int, then 4 padding so that the 8-byte pointer that implements the reference remains 8-aligned. Shame, because of course in practice `myIntegerAccessor` always refers to `myPrivateInteger`, so we wouldn't mind if the compiler elided it. But it can't for consistency of layout and because the class might be derived from with a different constructor. – Steve Jessop Apr 12 '16 at 11:25
  • @SteveJessop: "derived from with a different constructor" I don't follow. Any derived class must construct its base using some constructor of that base, and there being only one here, I think the compiler can _know_ for certain that `myIntegerAccessor` will always be a reference to `this->myPrivateInteger`. In fact I think the timings strongly suggest that dereferencing a stored pointer (for the reference) is being avoided here. Also, since one cannot take the address of a reference, I don't really see why any space needs to be reserved for the `myIntegerAccessor` field. – Marc van Leeuwen Apr 12 '16 at 12:31
  • @MarcvanLeeuwen: oh, you're probably right, I was thinking of two different things at once (derived classes and T.C.'s point that the default copy ctor is broken), I was probably confused. But my guess is compilers still won't elide reference data members, because normally they don't want the size of the structure to depend on successful optimization, only on some published ABI. Which is what I meant by "consistency of layout". And as written the reference *cannot* be elided because of T.C.'s point: a copy refers to its original. – Steve Jessop Apr 12 '16 at 13:03
  • @SteveJessop: Right. And I overlooked the existence of the default copy constructor (though it would be deleted like the default assignment, but it is in fact valid though semantically incorrect). So that would suggest that the reference optimisation is only possible because the compiler can _see_ the construction of the class instance, inside the testing procedure `testTime`. In that case, to do a more useful benchmark would consist of instead passing those class instances into `testTime` by reference. – Marc van Leeuwen Apr 12 '16 at 13:32
  • downvoted for confusing class names (I had to scroll back to the top to see that `MyClassA` was the one that should have been called `AccessorFunc`), and spending a huge amount of space in the answer on a useless benchmark that lets the compiler optimize the reference away (because it can see the object being constructed, so it knows what the reference is pointing to). And the benchmark doesn't do anything to stop the access from optimizing away entirely. `-O3` doesn't just speed up all code by the same amount. **This would be a good question without all the wrong microbenchmarks.** – Peter Cordes Apr 13 '16 at 06:29
  • @PeterCordes Would it be better to retroactively edit the question to remove the bench-marking? I was aware at the time I was asking that this only applied without any `-OX` flags for the issue of relative optimization between them. I when I get on my computer later today, I will edit the class names to make it more clear which is being referred to at a given time. Thank you greatly for your feedback. – Lilith Daemon Apr 13 '16 at 12:51
  • Yeah, I think you should dramatically shorten the question. Maybe just mention that the previous version of this question had a bunch of benchmark code and discussion which turned out to be not useful, "but see the revision history to see what the comments are referring to". Ideally, just present the idea, with a short summary of the alternatives. Keep the code compact. Matthieu M's excellent answer covers plenty of ground, so for future readers it's best to keep the question short and let that answer cover that ground. – Peter Cordes Apr 13 '16 at 12:57
  • Basically, it's an interesting idea that makes a good question, even though it turns out that the right answer is: "it's fundamentally flawed with unavoidable performance downsides, and major semantic/usability problems". – Peter Cordes Apr 13 '16 at 13:04
  • @PeterCordes I think I have it mostly fixed, would you possibly be willing to tell me what else I should change? – Lilith Daemon Apr 13 '16 at 13:32
  • Present your idea in the first code snippet at the top. Most people know what getter/setter methods are. When I first skimmed the question (when it was still the full question), I looked at the first code snipped trying to figure out what was interesting. It wasn't until I got to the 2nd code snippet a while later that I had any idea what your idea was. Don't force people to wade through a basic review of C++ before they get to the meat of the question. So, `MyClassReference` first, then some discussion, then the getter implementation in case anyone's not clear on what you're comparing to – Peter Cordes Apr 13 '16 at 13:39
  • BTW, kudos for taking the time to tidy up your post to make SO a better site. – Peter Cordes Apr 13 '16 at 13:41
  • @PeterCordes How does that look? Also I am willing to take as much time as necessary to help future users. I initially learned to code by reading SO, and am eternally grateful for this community and little would please me more than being able to assist some future user. – Lilith Daemon Apr 13 '16 at 13:54
  • 1
    Yup, that's solid. Now someone skimming can just say "ok, there's the idea being asked about", and skip down to the answers, with the assumption that the question is mainly "is this technique any good". The extra background and discussion is fine, along with the extra things you asked, since now they don't get in the way of skimming by people that know C++ well enough to grok the idea from just the code. Always try to optimize your posts for people skimming, because people have to do that while figuring out if the post is the sort of thing they're looking for in the first place. – Peter Cordes Apr 13 '16 at 14:08
  • @PeterCordes Understood, thank you greatly for your assistance, and for taking the time to help me improve my question. – Lilith Daemon Apr 13 '16 at 14:29

4 Answers4

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.

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.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • In fact, this flexibility is the entire reason for having getters and setters, isn't it? That's the logic I've always heard for Java, anyway. – Patrick Collins Apr 12 '16 at 07:18
  • @PatrickCollins: Yup. And the annoyance of having to make syntactic function calls for stuff like this is why languages like C# and Python (among others) offer properties, which are used syntactically like attribute access, but are just syntactic sugar for no argument function calls. – ShadowRanger Apr 12 '16 at 10:34
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).

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

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • I think you can write an assignment operator. You only need to modify the private member; the public reference member will then pick up that change. – Martin Bonner supports Monica Apr 12 '16 at 09:48
  • @MartinBonner: True, no need to reseat it here. – Matthieu M. Apr 12 '16 at 12:04
  • The reference version will have worse performance in real use, even apart from the object size problem. When the compiler can't see the construction of the object, it can't know that the reference is always a reference to the class member in the same class. **This means that even optimized code has to actually load the pointer and dereference it** (references are stored as pointers). This extra layer of indirection doubles the load-use latency (if the object was already hot in L1 cache). – Peter Cordes Apr 13 '16 at 05:00
  • @PeterCordes: In micro-benchmarks, maybe? In the midst of other load operations, this latency may very well disappear. It can after all be *pre-loaded* before you need to use the value (no side effect). That's why I am reluctant to attempt to measure it, micro-benchmarks are not always good indicators of real code performance. – Matthieu M. Apr 13 '16 at 06:06
  • It may or may not have a measurable effect. It definitely can't be better than an accessor function that optimizes away to a load from the private member. You don't need to create a microbenchmark to measure it when you can definitively say that it can't optimize away. If nothing else, code-size always matters at least a little. If this technique had any advantages, it would be worth trying to measure the overhead, but it has no upside (other than performance of un-optimized code, well-spotted). – Peter Cordes Apr 13 '16 at 06:20
  • Excellent answer! Minor note on inlining, though: stack traces *can* be preserved on inlined methods, DWARF2 (and other formats) support it. The reason inlining is not always performed is that it increases code size. – Oak Apr 14 '16 at 07:00
  • @Oak: Somewhat yes. But the hoist of transformations taking place on the code once you get inlining (merging constants, removing checks, hoisting part of the inlined body out of the loop, ...) may transform the code beyond recognition. – Matthieu M. Apr 14 '16 at 07:13
  • Okay, fair enough. – Oak Apr 14 '16 at 07:27
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.

Andrei R.
  • 2,374
  • 1
  • 13
  • 27
  • 4
    And furthermore, the default copy constructor is dangerously broken and the default assignment operator is no longer generated. – JDługosz Apr 12 '16 at 07:16
4

To answer question 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.

Michael Albers
  • 3,721
  • 3
  • 21
  • 32
  • 2
    That would imply accessor functions returning `const` references to member data were equally flawed, but they're often desirable to avoid copying. In general, C++ access specifiers are there to protect against accidental abuse, not deliberate casts intending abuse. – Tony Delroy Apr 12 '16 at 03:03
  • @TonyD All true. I guess it depends on how risk averse you are. – Michael Albers Apr 12 '16 at 03:05
  • Is an interesting point though. I didn't even think of the possibility of trying to intentionally break the const reference to allow writing. (Mostly for the reasons TonyD stated.) – Lilith Daemon Apr 12 '16 at 03:07
  • classes often use "getters" that return const references for complex types `const std::string& name() const;` for example. Removing the const breaks your guarantees. – Ryan Haining Apr 12 '16 at 03:32
  • @RyanHaining That's pretty much what Tony D said. – Michael Albers Apr 12 '16 at 03:33
  • 2
    `*reinterpret_cast(&mcb) = 4;` is also *likely* to work (on most implementations). Does that mean we can't have member variables at all? No, it doesn't. – user253751 Apr 12 '16 at 03:51
  • @MichaelAlbers: You can protect against Murphy, but not Machiavelli. Even if you hide implementation details I can create a struct that has the same layout and use a `reinterpret_cast` from a pointer to your struct to a pointer to mine. Of course, it'll be Undefined Behavior, but it'll probably work.... but why, as a library provider, should I care about a client subverting my code? They're shooting themselves in the foot, let them learn. – Matthieu M. Apr 12 '16 at 08:01
  • We *happen to know* that the `int` it refers to is defined as non-const, but it might not be. If `mcb` were a `const` instance of `MyClassB` then this would be UB, so we're discarding that piece of Murphy-protection. Or if `MyClassB` were documented without mentioning that `myIntegerAccess` is a reference to a private data member then we wouldn't be "entitled" to know anything. So this code is kind of Machievellian in that it's liable to rely on something that isn't actually guaranteed by `MyClassB`, but we don't know for sure :-) – Steve Jessop Apr 12 '16 at 13:47