Non object-oriented style

Pages: 12
closed account (y7jNhbRD)
Hello! As many are probably aware, there's a religious sect bent on converting everyone (and everything) to the wonders of OO programming. I personally (mostly) find OO concepts rather useful myself (with the exception of getters/setters for small classes), and am thus left guilty if I consider a more procedural technique. For example, let's say my goal is to implement a random number generator. A less OO technique that feels more intuitive might be this (C++ pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <random>
#include <iostream>
namespace Rand
{
std::mt19937 engine;

int Random (int min, int max);
float Random (float min, float max);
//...etc.
}

int main ()
{
std::count << Rand::Random(2,3);
}


This is as opposed to an instantiable class implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <random>

class Random
{
private:
    std::mt19937 engine;
public:
    int Random (int min, int max);
    float Random (float min, float max);
};

int main ()
{
Random rand;
std::count << rand.Random(2,3);
}


So my question is, would the former approach be a sinful act? There's also the option of using static methods in a class, but I see no advantage over namespaces (except the extra encapsulation, which doesn't seem truly useful in many cases, although I'm generally programming on my own). My doubts are perpetually raised by how languages such as Java don't even allow anything outside of classes.

One problem with the former approach that I can see myself is that if I decide to create a different engine for each module, then I'd have to modify all of the instances of the use of Random(int,int). But that would also happen with the class implementation, no?

Anyway, it would be good to get other opinions on it!
Last edited on
The so-called 'wonders of OO' have already been dismissed in more recent programming. Nowadays, a more procedural approach would be favoured, all based on the specific use case. This doesn't mean the OO is bad, just that often there is a better option, and you shouldn't constrain yourself to it.

In the case of a random number generator, in this case you would often use just a function with static data members. So, the random number generator code I use most often is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <random>
#include <chrono>
#include <type_traits>
#include <iostream>

template <typename T>
T randRange(T min, T max) {
    // make an assertion about the type of the argument
    static_assert(std::is_arithmetic<T>::value, "'T' must be an arithmetic type!");

    // a random generator instance, kept static across the function.
    static std::mt19937 randGen 
        (std::chrono::system_clock::now().time_since_epoch().count());

    typename std::conditional<
        std::is_integral<T>::value,
        std::uniform_int_distribution<T>,
        std::uniform_real_distribution<T>
    >::type range (min, max);

    return range(randGen);
}

int main() {
    // example use
    std::cout << randRange(2, 5) << '\n';
    return 0;
}
Welcome traveler! It is good that your travels have brought you here into our house. Know you that the sin lay not in the choice of one over the other, but instead it be the consideration of either one. To see the great mersenne_twister_engine, born of the purity that is the mighty templated class, shown in such a limited light by the casting of it's return type is decadent and profane!

For truth know that the second choice is by far worse, for it steals the traits of the twister as a theif instead of inheriting them as a true born child object would. Be that as bad as it may it does not cease it's corruption there! To forsake the the semicolon at the end of a class declaration is pure heresy and know that you will be humbled by your chain of tools should you attempt to commit such a treason!

NOTE: Sorry, I'm reading the Divine Comedy right now and after your introduction it seemed like a funny way to respond. The point is that this is already a class so I don't see a point in wrapping it. Is there another example you have?
I would refer the object-oriented approach just for the fact that you can have independently-seeded generators.
How would that be different from calling "srand()" multiple times from within the same instance? That's an honest question, I rarely work with RNG's and so I don't know if the twister is significantly different.
How would it not be different? AFAIK you can't portably get the seed use by the (currently deprecated) rand function, so there's no way to have independently seeded versions of it.

And yes, the different random generators are very much significantly different, so much so that the standards committee decided to include them all at the same time.
Last edited on
closed account (y7jNhbRD)
Oh brother... I clicked the wrong thing, and what I wrote got deleted! *faints*

Let's try again. NT3, you have a point! I did hear it said that OO isn't that great after all, but is there any evidence for this? Aside from trying to use it in unnatural circumstances, or overuse. And I used to use a static engine in the function, but that left me with truly little control. I'm writing a game engine, and being able to manage the engine can be somewhat important.

Computergeek01, thank you for the most elaborate welcome XD I've encountered this forum a number of times now, and it seems considerably friendly! Sometimes one just wishes to take a break from the to-the-point nature of Stack Overflow :P

However, wrapping things like the mt19937 I find a good practice. One can then easily use it without having to bother with the specifics, and it can be replaced easily without repercussions. Indeed, I stopped using mt19937 yesterday in favour of TinyMT, and all it took is a slight modification to the definition - the usage remained unchanged, which was quite convenient.

And as far as I am aware, rand() overall is quite terrible - its period is very small, and are especially unsuitable for 3D random generation. Also, it isn't absolutely uniform, with bias towards smaller values (I think). Indeed, as you mentioned, I had to do some srand() magic with random values multiple times to even produce proper results for textual applications. But that's merely a hack.

Speaking of other examples... *thinks* How about this:

1
2
3
4
5
6
7
8
9
10
11
namespace GPUMeshIndices
{
typedef unsigned Object3DInstance;

std::vector<Object3DIndex> indices;

bool initialise();
void clear();
void registerObject(Object3DInstance obj);
void unregisterObject (Object3DInstance obj);
}


Vs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef unsigned Object3DInstance;
//Singleton:
class GPUMeshIndices
{
public:
static GPUMeshIndices* createInstance();

bool initialise();
void clear();
void registerObject(Object3DInstance obj);
void unregisterObject (Object3DInstance obj);
private:
    GPUMeshIndices();
    GPUMeshIndices(const GPUMeshIndices&);
    std::vector<Object3DInstance> indices;
};


Now, this is a slightly artificial example. But I think it helps to illustrate the point.
OK, I think I see where you're going with this. I was assuming that you meant seeding multiple objects one right after the other, and since from what I see most people simply use "time()" for this doing it that way would not give you significantly different results. But if you waited a variable amount of time between seeding the different objects then I suppose it would work.

I don't know where you would ever want more then one chain of independently randomly generated numbers. But that's probably just because I've never come across a need for it.

I'll defer to you on this one.
closed account (y7jNhbRD)
I don't know where you would ever want more then one chain of independently randomly generated numbers. But that's probably just because I've never come across a need for it.


I don't expect to need them in the near future, but I see three possibilities for a game engine:
1. Having one generator for all the modules apparently has a slight negative impact on logic where it is used. At least a respectable game developer mentioned that it created bugs for his team, although for all I know he could have been using rand(), so I'm not sure how true this is.
2. Sometimes it's desired to have control over this for predictability. For example, game replay. Having a specific instance of it for the game logic separate from all other logic will allow to reasonably reproduce a scenario with a given seed.
3. Having a generator for each thread would avoid mutex locking, thus potentially increasing performance.

Although honestly, I merely used it as an example because I was faced with the dilemma recently. I settled for the namespace approach in this case, but was faced with similar decisions in the past, and was curious.
Minecraft is an example - the terrain generator uses a separately seeded random number generator from the mob AI.
To answer the original question:

So my question is, would the former approach [to an RNG] be a sinful act?


It's not that it's sinful, it's just that the OOP version is more flexible.

In a program I'm working on now, I am randomly generating maps. This is a lengthy process in which I used an RNG very frequently. Since it's such a lengthy process, I intend to do it in a separate thread to generate the sub-dungeons in-game in the background while the user is playing the game.

This means I will need to produce random numbers in 2 different threads at the same time. One in the generation thread, and one in the gameplay thread.

If I'm using the procedural style rng, this has 2 major problems:

1) It's not threadsafe. If both threads happen to call the RNG at the same time (which is likely), it will cause a race condition and therefore potentially have disasterous results. I could work around that with mutexes, but that would be SLOW if I had to lock/unlock a mutex every time I needed a random number. Not to mention that would be extremely difficult to do in code, since it's error prone.

2) It breaks determinism. If I have a dedicated RNG, I can give it a seed, run my random map generator, and know in advance the map I'm going to get, since the same seed=same string of random numbers. This is very useful for debug reporting, since if there's a problem with the map, I need only capture the seed used to generate it, rather than the entire map. If I have the seed, I can recreate the map at will. However if another thread is poking in and taking numbers from my generator... I lose that guarantee, since that will change the string of numbers generated for my maps.


Both of these problems instantly go away with an OOP approach. Each thread gets its own RNG object. Therefore there no threading issues or race conditions... and no one can interfere with my generator's RNG sequence.



This is the very heart of OOP: Not only encapuslation, but modularization. The ability to produce any number of objects and have them all independently operational. With a procedural approach you get one and only one. With OOP you can have an infinite amount.
Last edited on
And Notch is an example of a terrible game engine designer, so what's your point? You should know how bad that terrain generator is just by looking at the complete absence of contour lines on the earlier versions (has that ever been addressed by the way?) and how often the patches being applied are to balance game play because things are just way too random. A game should never become unplayable at 3:00 on a Sunday or whatever just because that is when the RNG decides to spit out a dozen top level monsters. An even distribution of numbers only ensures that the player is just as likely to get griefed as they are to get a playable game.

Don't even try to play the "It's success speaks for itself" card either. The concept of the game play is why Minecraft is good, not the implementation.

A programmer should pre-populate an array based on how often they want a given event to occur and shuffle it. That way the RNG is only responsible for the variance in the distribution but ultimately the programmer is responsible for the frequency of an occurrence. Then regenerate and reshuffle some time before the end of the stack so as to avoid any "card counting".
closed account (y7jNhbRD)
Disch, you're tempting me to actually go OO with this :P As mentioned, I've a suspicion that I'll need a number of them later on, and it would indeed solve it nicely.

Computergeek01, while pre-computing an array seems a good idea for some cases, I think that it will most likely increase memory usage at least a little, and the period wouldn't be truly good. Why not generate just in time?

Disch, do you think that the OO way would still be useful in cases where only one instance is guaranteed to be needed? As in my second (artificial) example, for instance.
@ OP: Because generating just in time breaks game-play and you run into issues like what Disch is seeing where you're spending hours of production time testing every instance for results that don't make sense. The idea that I use isn't something that I came up with, it's torn right out of the pages of every pen and paper RPG I've ever seen (I want to say WoC came up with it for D&D). You pre-populate a list of events, then roll the dice to see which event occurs and where it occurs. You then roll the dice against another list of enemies, loot etc. This was mostly intended to save time in coming up with missions but it also has the effect of ensuring that the results are always balanced because you compare the players levels against a difficulty modifier to scale the scenarios so that they are more interesting and never "grindy" or "griefy".

EDIT: Memory is a minor concern since ideally you're only storing unsigned integers that indicate the objects. You're not storing the entire environment in the table.
Last edited on
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
namespace procedural // fine, if this is all that is required
{
    namespace Rand
    {
        namespace { std::mt19937 default_engine( std::time(nullptr) ) ; }

        int Random( int min, int max, std::mt19937& rng = default_engine );

        // ...
    }
}

namespace class_based // often better, for reasons outlined in earlier posts
{
    class Rand
    {
        private: std::mt19937 engine{ (unsigned int)std::time(nullptr) };
        public:
            int Random (int min, int max);
            // ...
    };
}

namespace generic // in a non-trivial program, I would favour something like this
{
    template < typename NUMERIC_TYPE = int, typename ENGINE = std::mt19937 >
    class Random
    {
        private: ENGINE engine;
        public:
            Random( ENGINE&& rng = ENGINE( std::time(nullptr) ) ) : engine( std::move(rng) ) {}
            NUMERIC_TYPE operator() ( NUMERIC_TYPE min, NUMERIC_TYPE max ) ;
            // ...
    };
}

namespace object_oriented // I don't particularly care for this kind of approach
                          // towards generating uniformly distributed random numbers
{
    class Rand
    {
        private: std::mt19937 engine{ (unsigned int)std::time(nullptr) };

        public:
                virtual ~Rand() = default ;
                virtual int Random (int min, int max);
                // virtual ...
    };
}


Please note that object-oriented programming is not a panacea. "OOP" does not simply mean "good" - if there are no inherent hierarchical relationships among the fundamental concepts in your problem then no amount of hierarchy and virtual functions will improve your code.

The strength of OOP is that there are many problems that can be usefully expressed using class hierarchies - the main weakness of OOP is that too many people try to force too many problems into a hierarchical mould. Not every program should be object-oriented.

As alternatives, consider plain classes, generic programming, and free-standing functions ... - Stroustrup
http://www.stroustrup.com/bs_faq.html#Object-Oriented-language
Disch, you're tempting me to actually go OO with this :P As mentioned, I've a suspicion that I'll need a number of them later on, and it would indeed solve it nicely.


The only downside is that you'll have to pass around an RNG object instead of referring to a global instance of one.

I am not a fan of globals.

Disch, do you think that the OO way would still be useful in cases where only one instance is guaranteed to be needed? As in my second (artificial) example, for instance.


Singletons are weird because they're sort like fake-OO. There isn't really any difference between a singleton and a series of global functions inside a namespace.... so your two examples seem equivalent to me.

The question I always ask before using a singleton (or globals) is "am I sure that I will only ever want one of this object? No matter the circumstances?".

The answer to that question is usually almost always "no". Even for something like a 'Game' object which runs the overall game logic flow --- I can conceive of situations where more than one of them might be useful:

- I used to be in the retro emulation scene, and people would make emulators that would run several different games in parallel. This is easily accomplished with OOP design -- just make a new Emu object. But if you use globally driven logic, you're hosed.

- I don't know if you've ever played games like Braid or Prince of Persia: Sands of Time. One of the gimmicks in those games is the ability to rewind time. One way this can be accomplished is by making "movies" which can playback user input. Run that user input through a deterministic world and you'll get the same output. Rewinding is then just generating that movie and playing it in reverse. To make that easier, you could have multiple 'Game' objects running different parts of the movie at the same time.



About the only time I thought I really would only ever want 1 of something is for things like a resource manager... where you have read-only resources that are loaded from a file or something. Where there is literally zero value in having the same resources loaded multiple times in memory, regardless of how many games/objects are accessing it.

But even with that.... you can run into multithreading issues.





But it'd be very easy to argue that I'm planning ahead for too much. Most games don't need multiple 'Game' objects. Most games don't need multiple resource managers. And in those cases... a global/singleton is "good enough". It gets the job done and is easy to implement.

It's just... my personal preference is to avoid them. Sure, I may not need all the flexibility that unique objects provide... but it isn't really hard to use them. And if I ever do need them, then I have them.
@Computergeek01: I don't know what your problem is or why you have to bash everything I say. Disch and I both provided valid examples of why you would need more than one independently-seeded random number generator, and you chose to nitpick the particular game and developer from the example I used.

I don't really understand where this thread is going anymore, and I have nothing more to say on the subject than what I've already said. Sorry for drama ;p
Last edited on
closed account (y7jNhbRD)
@Computergeek01 I see what you mean. While this form of precomputation does not sound applicable to lower level logic and simulation, it does seem useful for higher level balancing and gameplay, so thanks!

@JLBorges Thanks for the examples and the link! This greatly clarifies some things. I'll be reading that paper Mr. Bjarne mentioned at the site, which seems interesting. He said that java-styled programming is as detrimental as C style within C++ - does anyone know of any examples? While I can imagine some disadvantages, it's difficult to see how it would be equally detrimental.

@Disch Thank you for the extensive explanation. Planning ahead is certainly something I should do more of - currently I'm forced to reimplement major features because they just weren't viable. As I understand, these things seem to grow exponentially in importance as the program grows more complex, so I guess I would indeed be better off with a class interface.

@ LB: I'm not trying to bash you, in fact when you first came in I was genuinely interested in your opinion of the new RNG's since I'm still shuffling my arrays with "srand()" and "rand()". I do think that the approach of filtering out invalid data on the fly when the valid set is known ahead of time is the wrong way to approach this problem though.
Ok, maybe I read different emotion in your words than you actually meant - it did sound harsh though.
Pages: 12