gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

C/C++ > OOP Principle: Encapsulation - is it reasonable for C++ ?

#114745 - Izhido - Wed Jan 10, 2007 4:36 pm

Hi!

I was reading this the other day:

http://codebetter.com/blogs/raymond.lewallen/articles/59908.aspx

Among other things, it explains that, under the principle of encapsulation, we shouldn't allow any member variables of a class to be public, and access to them should be restricted to "getters" & "setters".

Now, everyone around me is always telling me that, while good in theory, in practice using functions to get and/or set member variables will actually make your program (slightly) lengthier and (slightly) slower, these being commodities a C++ developer cannot afford to lose on resource-constrained devices like GBA, DS, PDAs, phones, and many others.

But, if I understand things correctly, C++ compilers are becoming, day by day, wild beasts of pure, raw optimizing power, right?. Would it be too far-fetched to think that modern C++ compilers are smart enough to strip away pieces of code that, from a machine-language point of view, are not really required, like, say, providing direct access to the memory address of a member variable, instead of forcing a call to the getter/setter functions IF all they do is just get/set the value of that variable?

I'd be really happy if that would be the case, since that means I can use OOP principles on my applications without remorse of any kind. I could use getters/setters for all external access to my classes's variables, allowing me to both A) not worry about related performance issues, and B) alter the behavior of getters/setters without any hassles, knowing that everything will work OK (or as OK as it could get :) ).

What would you guys think about it? Am I right to be hopeful about this?

- Izhido

#114754 - sgeos - Wed Jan 10, 2007 5:11 pm

Maybe, but I'm sure that it depends on what you have. I don't think that something like this could be optimized:
Code:
interface I
{
   int getValue(void);
}

class A implements I
{
   int mValue;

   int getValue(void)
   {
      return mValue;
   }
}

class B implements I
{
   int getValue(void)
   {
      return 0;
   }
}

class C extends A
{
   static int mSharedValue;

   int getValue(void)
   {
      int value;
      value = Super.getValue() + mSharedValue;
      return value;
   }
}

I may or may not have a variable. A may or may not have just one variable. B can be optimized if you know you have B. C does more than get a single variable.

-Brendan

#114758 - Izhido - Wed Jan 10, 2007 5:31 pm

Agreed. I would think that, too. But what of this?

Code:

class A
{
    int X;

    int getX();
    void setX(int NewValue);
};

int X::getX()
{
   return X;
}

void X::setX(int NewValue)
{
   X=NewValue;
}


Would that be optimizable?

- Izhido

#114760 - sgeos - Wed Jan 10, 2007 5:40 pm

As long as no sub-class of A overrides getX(), or setX() it is optimizable. Whether modern compilers catch this particular optimization or not, I don't know.

-Brendan

#114767 - Miked0801 - Wed Jan 10, 2007 6:45 pm

Guys, premature optimization of code is a root of pure evil. It is better to write code that is easier to maintain and modify and then go back and change the 1 or 2 areas that need extra help than too throw away the whole OO idea because it is a bit slower in some circumstances.

I'll take 1% (or even 10%) slower overall and have code that follows OO maintainability and flexibility.

#114776 - tepples - Wed Jan 10, 2007 8:04 pm

Izhido wrote:
under the principle of encapsulation, we shouldn't allow any member variables of a class to be public, and access to them should be restricted to "getters" & "setters".

Now, everyone around me is always telling me that, while good in theory, in practice using functions to get and/or set member variables will actually make your program (slightly) lengthier and (slightly) slower

If they're not virtual, they'll probably be inlined.

Quote:
Would it be too far-fetched to think that modern C++ compilers are smart enough to strip away pieces of code that, from a machine-language point of view, are not really required, like, say, providing direct access to the memory address of a member variable, instead of forcing a call to the getter/setter functions IF all they do is just get/set the value of that variable?

To an extent, yes, compilers are gaining automatic inlining.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#114780 - DekuTree64 - Wed Jan 10, 2007 8:16 pm

One thing to be aware of is that code defined in one .cpp can never be inlined in another .cpp, because they know nothing of eachother's existence until linker time. So for get/set functions to be inlined, they must be defined in the header:

Foo.h:
Code:
class Foo
{
public:
    int getX() const;
    void setX(int value);

    void printX() const;

private:
    int x;
};

#include "Foo.inl"   // Good habit to keep things organized


Foo.inl:
Code:
inline int Foo::getX() const
{
    return x;
}

inline void Foo::setX(int value)
{
    x = value;
}


Foo.cpp:
Code:
#include "Foo.h"

void Foo::printX() const
{
    cout << x;
}


Then when some other file uses Foo, setX and getX will be inlined if the compiler decides it would be advantageous (for such simple functions, this should be always), and printX will never be inlined, because that other file doesn't know what it does.

One thing I'm curious about though... where does the code from an inline function go if the compiler decides not to inline it? Is a copy made for each file it's called in, or is the linker smart enough to only keep a single copy as if it were defined in a .cpp file in the first place? Difficult to figure out because it has no global symbol in the linker map file.
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku

#114782 - sajiimori - Wed Jan 10, 2007 8:21 pm

Agreed that inlining works fine in modern compilers. But when you care about whether something is inlined, check the output.
Quote:
Now, everyone around me is always telling me that, while good in theory, in practice using functions to get and/or set member variables will actually make your program (slightly) lengthier and (slightly) slower, these being commodities a C++ developer cannot afford to lose on resource-constrained devices like GBA, DS, PDAs, phones, and many others.

I hear statements like this a lot, and I always wonder which non-resource-constrained device the author is thinking about.

Every system has limited resources. Handheld developers seem to forget that console and PC developers do more with their hardware -- it's not as if they're making GBA games for XBox 360 and setting up hammocks around the office for all the relaxation time that results from the lack of resource constraints.

If you truly believed that you can't afford (slightly) lengthier and (slightly) slower code, you'd be writing everything in assembler. The reality is that writing it in C makes the object code way lengthier and way slower than writing in assembler, in the short run.

But in the long run, the time saved by developing in C (or C++) allows you to explicitly allocate time to optimize portions of the game that need it. If you're working on a large project with limited time, coding everything in assembler is likely to make your game slower because you're wasting time optimizing things that don't need it.

Every hour of optimization counts -- spend it wisely. =)


Last edited by sajiimori on Wed Jan 10, 2007 8:25 pm; edited 1 time in total

#114784 - sajiimori - Wed Jan 10, 2007 8:23 pm

Quote:
One thing I'm curious about though... where does the code from an inline function go if the compiler decides not to inline it?
Most often, I've seen compilers act as if the function was declared 'static' -- each module that calls it will get its own copy.

It's also possible for compilers to generate a single global copy, which is how non-inlined templatized code typically works these days. The linker will strip out all the instances after the first one it sees.

#114804 - Izhido - Wed Jan 10, 2007 9:19 pm

DekuTree64: AHA! So that's why I keep on seeing ".inl" files all over the place! It seems a good idea; I don't particularly like to be forced to implement functions in header files. I think I'll give them a try. Thanks!

sajimori: Yes. I use the term "resource-constrained devices" a lot. Maybe it's wrong, I don't know. Primarily, I use the term for small, portable devices; anything "below" a PC is tagged by me as a resource-constrained device.
(And, please, I beg you, don't make me explain what "below" means in that context. That's precisely why I use this term. :D )

#114806 - tepples - Wed Jan 10, 2007 9:30 pm

sajiimori wrote:
Every system has limited resources. Handheld developers seem to forget that console and PC developers do more with their hardware -- it's not as if they're making GBA games for XBox 360 and setting up hammocks around the office for all the relaxation time that results from the lack of resource constraints.

They're making business applications, where the ratio of programmer time to runtime is tilted more toward programmer time than it is for real-time games. And they're making Hexic and Geometry Wars, which don't come close to using all the power of the Xbox 360; they're just on the higher platform because the higher platform has a download shop.

The term I prefer is "battery-powered devices" or "fixed hardware" or "embedded systems" depending on the context.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#114811 - Izhido - Wed Jan 10, 2007 9:47 pm

To add more to the confusion: did you know that the Xbox 360, with all its processing power & graphics & whatnot, uses a *STRIPPED DOWN* version of the .NET *COMPACT* Framework for managed applications (games)? Quite mindboggling, if you ask me :)

Sometimes it's hard to come with a consistent way to classify computing devices.

- Izhido

#114813 - keldon - Wed Jan 10, 2007 9:53 pm

Well there comes a point where there is enough processing power to 'play' with, and you can begin to focus on the project itself and less about the hardware limitations.

#114819 - sajiimori - Wed Jan 10, 2007 10:41 pm

Quote:
They're making business applications, where the ratio of programmer time to runtime is tilted more toward programmer time than it is for real-time games. And they're making Hexic and Geometry Wars, which don't come close to using all the power of the Xbox 360; they're just on the higher platform because the higher platform has a download shop.
And they're making Gears of War, and they're making F.E.A.R., and they're making World of Warcraft.

Yes, there are applications that don't push the hardware very hard. Brain Age for Nintendo DS is another example.

So again, it's not a matter of how many resources are available, or whether the system uses ROMs or DVDs, or whether it's battery powered, or whether it has upgradable components. It's a matter of how hard the application is pushing the hardware, and there are game developers on all major platforms that are concerned with pushing the hardware.

#114864 - sgeos - Thu Jan 11, 2007 5:08 am

Izhido wrote:
I use the term "resource-constrained devices" a lot. Maybe it's wrong,

It's not wrong, it just doesn't really mean anything if you think about it. "Low spec devices" or "especially resource-constrained" may better convey what you are trying to say- if you overly think things. Anyone with common a little sense should know what you are talking about even if they think the wording is goofy.

-Brendan

EDIT: Fixed name. =P


Last edited by sgeos on Thu Jan 11, 2007 12:45 pm; edited 1 time in total

#114877 - tepples - Thu Jan 11, 2007 6:37 am

sajiimori wrote:
Yes, there are applications that don't push the hardware very hard. Brain Age for Nintendo DS is another example.

If only they could have pushed the hardware a bit harder to recognize the word "blue" more reliably. Perhaps Microsoft could have helped; its Windows division seems to know all about the color blue.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#114902 - sgeos - Thu Jan 11, 2007 12:47 pm

Wasn't brain age made in about four months? Or was it four people? Or was it both? At any rate, it's not a budgetfest.

-Brendan

#114962 - sajiimori - Thu Jan 11, 2007 10:27 pm

Quote:
Anyone with common a little sense should know what you are talking about even if they think the wording is goofy.
Call them what you will: resource-constrained, low-spec, embedded... (I often say "small".) The same point applies regardless of terminology. If you're trying to push the hardware, then efficiency matters, period.

So, to recap...

Myth: One uses a higher-level language on larger systems because efficiency is less important on those systems.

Reality: One often uses a higher-level language because efficiency is very important.

If you have X number of hours to make your game fast, what would you rather do: view a performance profile and spend your hours on the areas that need attention, or randomly distribute your hours across the entire program?

#114979 - sgeos - Fri Jan 12, 2007 12:37 am

sajiimori wrote:
Reality: One often uses a higher-level language because efficiency is very important.

Fantastic point.

-Brendan

#114980 - gmiller - Fri Jan 12, 2007 12:45 am

I agree with sajiimori but would like to add that some times high level languages come with a lot of baggage and generally it is not obvious that it is there. I have been on projects where the high level language was chosen because it had all these "features" that got used and the system performance "sucked". You can produce a gem in performance in any language as long as you understand how it relates to the platform you are targeting, you can also produce a dog in performance in any language. Understanding your tools and how they work with the hardware can allow you to produce code that flies and low level languages are NOT a requirement to meet performance goals.

#114987 - ScottLininger - Fri Jan 12, 2007 1:30 am

Homebrew projects are primarily learning time, and there's no better way to learn than to try all kinds of goofy stuff.

So in that context, I'll throw out the idea that code optimization for optimization's sake is great. Once you've done it 20 or 30 times and the time comes to actually deliver a project, you'll have the background to know where it's worth focusing and where it's not.

The only way to know if your compiler will do a good job is to try it.

-Scott

#114992 - sajiimori - Fri Jan 12, 2007 1:42 am

Yes indeed! Experiment away. :)

#114995 - gmiller - Fri Jan 12, 2007 1:50 am

In the class I used teach in optimization we studied methods of determining where you are spending your time and then try to focus on what can be done to make things better. In embedded systems you re constrained on the tools you can use but some of the emulators have some good performance gather options. On the x86 I use Vtune and in the GBA I use the performance data the VBA emulator will gather to get a target area. Of course emulators are not the real thing so where you are "fixing" might not be the real issue. Having some sort of hardware harness for the platform you are targeting would be better but those types of setup are usually not in the casual developers price range.

#115033 - sgeos - Fri Jan 12, 2007 11:24 am

gmiller wrote:
Having some sort of hardware harness for the platform you are targeting would be better but those types of setup are usually not in the casual developers price range.

Even if you have the money to buy it, you won't be able find it easily.

-Brendan

#115035 - keldon - Fri Jan 12, 2007 11:52 am

Didn't the later Playstation Devkits have a so called 'performance analyser' that was key in identifying where time was being spent. In c++ the profiling wasn't too bad; I was able to identfy which methods took most of the processing time, but in java it was a pain; the program would move at snails pace, taking a minute to process a single frame with the profiler on!!!

And if only the page would load, I would be able to show you some amazing optimisations that were made by swedish developers with optimising a database engine to search at 2000 times the speed of Posgresql on extremely large queries.

#115097 - tepples - Sat Jan 13, 2007 12:16 am

sgeos wrote:
gmiller wrote:
Having some sort of hardware harness for the platform you are targeting would be better but those types of setup are usually not in the casual developers price range.

Even if you have the money to buy it, you won't be able find it easily.

If you have the money to pay programmers and artists in a developed country, then you probably have the money to write a business plan, make a prototype for PC or PDA, get office space, get a Nintendo license, and get a devkit.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#115155 - sgeos - Sat Jan 13, 2007 9:19 am

tepples wrote:
sgeos wrote:
Even if you have the money to buy it, you won't be able find it easily.

If you have the money to pay programmers and artists in a developed country, then you probably have the money to write a business plan, make a prototype for PC or PDA, get office space, get a Nintendo license, and get a devkit.

Sure, but if all you have is enough money to buy one devkit, you will have a hard time finding it.

-Brendan

#116366 - blargg - Wed Jan 24, 2007 10:03 pm

To get back to the original question, I've come across many articles recommending against get()/set() interfaces, but without really explaining why these are a problem, usually simply equating them with public members, which we all know are evil (never mind why, so we could make an informed decision). I've found a few authors who really explain the reason (Allen Holub's Why getter and setter methods are evil is good). A possible problem with designs that use public members or many get()/set() functions is that it results in more coupling between modules. Changes will affect more users than in a design which provides higher-level behaviors. It ties in with the general principle of designing systems so that complexity reduced by dividing it up into multiple modules that each interact in well-defined ways. The interactions should be carefully chosen so that they are likely to remain stable during changes to the system. The most stable interactions are ones that allow one module to request the services of another, with as few unnecessary details as to how that request is granted. In this light, public members and get()/set() functions have their place, but they are often inferior to other possible ways of organizing things.

In C++, using an inline accessor should be no less efficient than using a public member:

Code:
class X {
public:
    int i;
};

class Y {
    int i;
public:
    int get_i() const;
};

inline int Y::get_i() const { return i; }


This will not be the case if the accessor is virtual and the type of the object is not known:

Code:
class Z {
    int i;
public:
    virtual int get_i() const;
};

inline int Z::get_i() const { return i; }

void f( Z* z )
{
    z->get_i(); // compiler must make virtual call
   
    Z local;
    local.get_i(); // compiler can inline, since it knows complete type
}


As others have said, code for clarity, then it'll be easy to optimize that 5-10% that turns out to be where most of the time is spent. Or, optimize everything and be unable to make any high-level optimizations later, due to the (unnecessary) complexity of it all.

#116377 - sajiimori - Wed Jan 24, 2007 11:34 pm

Mr. Holub would be hard pressed to convince me that it's bad to have a setPosition method on my Creature class. Anything else is an obfuscation of the brain-dead simple fact that I want to change the position of my creature.

#116380 - gmiller - Wed Jan 24, 2007 11:50 pm

If the compiler can inline you can also possibly eliminate the aliasing concerns that object methods can create for you. Generally if the value is retrieved for you with a method the compiler must assume that it needs to get the value each time so either way (inline or method invocation) the data is retrieved. This means that loops that have limit values based on a method call will execute the method each loop iteration. also if you use the get method multiple time in a loop the code will be executed each time it is referenced by the method. It is generally easy to work around these issues but it is not always obvious that the problem is even there.

#116382 - sajiimori - Thu Jan 25, 2007 12:10 am

gmiller, I don't understand this sentence:
Quote:
Generally if the value is retrieved for you with a method the compiler must assume that it needs to get the value each time so either way (inline or method invocation) the data is retrieved.
Are you saying that the compiler can't cache data member values if they're accessed via methods, whether inlined or not? If that's what you're saying, I disagree. Modern compilers can optimize equally well when using inlined methods as compared to direct data member access.

It's fine to use such inlined methods in loops.

#116387 - blargg - Thu Jan 25, 2007 1:54 am

I doubt the author would unconditionally argue against all accessors. Whether it's the best design depends on the context. If most clients need to nudge a creature around rather than set its position, it would make more sense to have offsetPosition( delta_x, delta_y ) than have clients get and set the position. On the other hand if there's only one other client, Teleporter, which randomly moves creatures around, a setPosition() would make more sense. The point I got from the article was that accessors which merely get and set members might be less effective than operations that allow clients to more directly express their intent, since the former will result in unnecessary coupling and thus resistance to change. Obviously it's somewhat hard to predict how a system will evolve in directions you aren't planning for.

#116388 - sajiimori - Thu Jan 25, 2007 2:49 am

I've learned to avoid making design decisions based on which use cases I predict will be more common. It's better to provide a complete, minimal, and orthogonal interface, even if it makes some "common" use cases slightly more verbose.

Even if I guess right about the frequency of my own use cases, my assumptions will likely be wrong for other users, and they'll be cursing my name for getting cute with the design. =)

Since all movement operations are trivially supported by a get/set pair for position, but not all movement operations are supported by offsetPosition, the former is strongly preferred.

If someone wants to make an abbreviation for a common operation, they're free to define their own non-member functions.


Edit: To summarize my standpoint: Before adding a get/set pair, consider whether it's the best solution.

The author should consider whether there is a reasonably-sized and statically-known set of higher-level operations that could be provided instead of a get/set pair.

If not, then use get/set.

If so, then the author should further consider whether clients can use some combination of those operations to get the full functionality of a get/set pair -- that is, are they a superset of the functionality supplied by get/set?

If so, use get/set and write the other operations as non-member utilities. Providing them as the fundamental interface is an obfuscation of get/set.

An example of a bad substitute for get/set would be supplying these two methods:
Code:
Vector3 Creature::vectorFromPointToSelf(const Vector3& point) const;
void Creature::offsetPosition(const Vector3& moveBy);
Those methods can be used to implement get/set as external utility functions, and the net result is purely an obfuscation.

After deciding to use some variant of get/set, use the first of these schemes that's acceptable:

- 'get' only, return something by value. Completely hides internal representation.

- 'get' only, return const reference to abstract base of something. Hides concrete representation, but reveals that there is a concrete representation: it can't be a "virtual" property of the object, such as a position that's procedurally calculated.

- 'get' only, return const reference to conrete object. Reveals full representation, but no mutation is allowed.

- get/set pair. Mutation is allowed, but class author can enforce additional invariants, such as automatically sending a notification to other objects when its position is changed.

- public data, or a 'get' method returning a non-const reference. At this point, encapsulation and invariants are out the window and the difference between schemes is splitting hairs.

At any rate, calling get/set "evil" is a major overstatement.


Last edited by sajiimori on Thu Jan 25, 2007 3:23 am; edited 2 times in total

#116389 - tepples - Thu Jan 25, 2007 3:19 am

sajiimori wrote:
Mr. Holub would be hard pressed to convince me that it's bad to have a setPosition method on my Creature class.

You mean Creature::moveTo()?
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#116390 - sajiimori - Thu Jan 25, 2007 3:29 am

Actually, moveTo seems a little ambiguous compared to setPosition. It sounds like an instruction to attempt a pathfinding operation, but it could also be synonymous with setPosition.

#116391 - tepples - Thu Jan 25, 2007 3:47 am

In object-oriented programming, method names should reflect behaviors that an object is capable of doing. If it spawns, Creature(x, y) (the constructor) is appropriate. If it can walk somewhere, findPathTo(x, y) is appropriate. If it can teleport somewhere, teleportTo(x, y) is appropriate.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#116393 - sajiimori - Thu Jan 25, 2007 6:47 am

"Set" works as an instruction in a complete sentence: "Creature, set your position to x." In English, the word "change" might be slightly more natural, but they both work and "set" is an exceedingly common prefix. Virtually nobody is confused by it.

There's really no need to get cute with method names when all I want to do is set a position.

#116395 - tepples - Thu Jan 25, 2007 7:18 am

sajiimori wrote:
"Set" works as an instruction in a complete sentence: "Creature, set your position to x."

It's also a filler word, just as "is" is a filler word. The only way "set" carries any meaning is as a collection of distinct elements, none of which is the collection itself. The content word here is "position", which is a property, not a behavior. What behavior of the object results in setting this position?
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#116406 - sajiimori - Thu Jan 25, 2007 9:27 am

If you can grant that "change" has a substantive meaning here, then I will hereby define "set" as being synonymous, which is typical.

#116415 - sgeos - Thu Jan 25, 2007 12:56 pm

Teleport and move to could very well be the same thing:
Code:
Interface I
{
   bool move(int x, int y);
}

// move to
Class A implements I
{
   bool move(int x, int y)
   {
      // attept to teleport to xy
      // random teleport if xy is blocked
      return true;
   }
}

// teleport
Class B implements I
{
   bool move(int x, int y)
   {
      // find a path to xy
      // take a few steps in that direction
      // return true if steps taken
      // return false if no path
   }
}

// failure parasite
Class C implements I
{
   bool move(int x, int y)
   {
      // if rand(0, maxrate) < failrate, return false
      // else return host.move(x, y)
   }
}


-Brendan

#116441 - sajiimori - Thu Jan 25, 2007 7:53 pm

If that scheme solves a problem in your application, then go for it. If it doesn't solve a problem, it's merely obfuscation.

Either way, I'd start with something simple, like a setPosition method and a pathfindTo method. If an additional abstraction is needed to give those two methods a common interface, that abstraction should be provided separately, and the original interface should not be modified.

Again, it is not a good idea to get fancy with the fundamental interface to an object. Provide something clear, simple, and orthogonal. If something fancy is needed, implement it separately in terms of the fundamental interface.