Handmade Hero»Forums»Code
Mox
32 posts
Interfaces
Hi,

I know OOP is a black sheep here, but I would like to hear your views on some of the simpler forms of OOP. And maybe this could even be solved by non OOP methods.

How to group similar code implementations to be able to reuse and/or select one for testing. For instance different methods of random number generation. What if you wanted to replace the pseudo implementation we have now with a new one, and wanted to compare them.

From the usage code we have now concerning random series we already have somewhat of an API. And although some might argue agains it, this API could be represented as the following interface (in no perticular language pseudo code):

1
2
3
4
5
6
7
8
9
interface Random {
	void Seed(int);
	uint32 RandomNextUInt32();
	uint32 RandomChoice(uint32 ChoiceCount);
	real32 RandomUnilateral();
	real32 RandomBilateral();
	real32 RandomBetween(real32 Min, real32 Max);
	int32 RandomBetween(int32 Min, int32 Max);
}


The current implementation with the table could then be used like:

1
2
3
4
RandomTable series;

series.Seed(1234);
x = series.RandomChoice(2);


This is not really any different from the straight C code.

Changing to another implementation could then be a single line change:

1
PRNGRandom series;


The implications of using this setup with normal OOP practices with virtual functions already brings one downside: the vtable. But this could probably be implemented with just straight classes, without virtual functions.

If both RandomTable and PRNGRandom implement the same signatures, they could be used in the same places. The only problem I see is passing them to other functions. Storing the series in GameState could be done trivially and that would preserve the type information and thus the implementation used (function pointers will be hardcoded by compiler).

But passing them in functions without changing the parameter type would require some sort of BaseClass to be able to keep the parameters of the function the same. This will probably require templating like CRTP or the vtable which would like to prevent.

What are your thoughts on this? Is there a way to do this in straight C? Or would this really require search/replace of the implementation calls?


PS. I'm also open to the discussion of whether I should be thinking about this at all. Since I'm in the process of trying to go back to my coding roots by going away from OOP. But it is hard to completely give up, when I keep seeing re-usable parts of code like Random number generators, that already have a bit of OOP style in them with the "this" pointer as first parameter.
Casey Muratori
801 posts / 1 project
Casey Muratori is a programmer at Molly Rocket on the game 1935 and is the host of the educational programming series Handmade Hero.
Interfaces
Well, I guess the thing I would ask is, what is the benefit of making an "object" any more formal than just the struct and some functions? You already have the ability through function overloading to change which random number API you are using by changing the type (random_series to random_series_2 or whatever). So what is the benefit of making an "object" out of it?

If the answer is polymorphism, well, yeah, at that point you have a vtable situation and things start to get a lot more expensive, because the random number generation can no longer be inlined, for example - it always has to be a function call. If the answer is something else, what is that something else?

- Casey
Mox
32 posts
Interfaces
I guess that is what OOP does with your mind, I dind't even consider straight overloading as a good way to change the implementation of the interface.

Although that does indeed give you the same ease of changing implementation when using the random_series_2 from the GameState, it does not solve the passing as a paramater without changing the type of the parameter too. I guess the lazy programmer in me would like the compiler to infer types more. The compile time polymorphism of the CRTP pattern does look appealing, though it would probably make the type declaration of the function that takes it as a parameter quit a mess. Ugh, templates :(

Well I guess the preprocessor would come to the rescue. Just #define'ing the type you want to use in one place would solve it. Like I said, going the OOP way has taken its toll and I have to deprogram my brain. But I love this series for showing me this non-OOP way as it never sat right with me...
Casey Muratori
801 posts / 1 project
Casey Muratori is a programmer at Molly Rocket on the game 1935 and is the host of the educational programming series Handmade Hero.
Interfaces
Yeah, ML and friends do type inference in a much better way, without making you do all kinds of template nonsense and so on. But there's a whole other set of things you have to worry about if you go that direction. It would have been nice if C++ had introduced a happy medium, but of course they always do the worst possible thing so they didn't :(

- Casey
Krzysiek
17 posts
Interfaces
I'm not sure what you want to achieve. If you really need to inline these functions then I have no idea but if you can use pointers why not just use them explicitly in plain C style, e.g. like this:

 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdint.h>

typedef uint32_t uint32;
typedef int32_t int32;
typedef float real32;


typedef struct
{
    void (*Seed)(int);
    uint32 (*RandomNextUInt32)();
    int32 (*RandomBetween)(int32 Min, int32 Max);
} random_t;


static uint32 RandomNext1()
{
    printf("RandomNext #1\n");

    return 0;

}
static uint32 RandomNext2()
{
    printf("RandomNext #2\n");

    return 0;
}

static void Seed1(int s)
{
    printf("Seed #1:%d\n",s);
}

static void Seed2(int s)
{
    printf("Seed #2:%d\n",s);
}


void test(random_t *rnd)
{
    if (!rnd->Seed)
        return;

    printf("**** testing\n");

    rnd->Seed(127672);

    if (rnd->RandomNextUInt32)
        rnd->RandomNextUInt32();

    if (rnd->RandomBetween)
        rnd->RandomBetween(1,10);
}


int main(void)
{
    random_t random = {
	.Seed = Seed1,
	.RandomNextUInt32 = RandomNext1,
	.RandomBetween = 0
    };

    test(&random);
    random.Seed = Seed2;
    test(&random);
    random.RandomNextUInt32 = RandomNext2;
    test(&random);

    return 0;
}
Mox
32 posts
Interfaces
Yeah, I guess that will work. But isn't that just implementing your own vtable?

I was hoping for a better way to change implementation, but I think the compiler is not smart enough for the type inferring that is required here.

Inlining is not really a hard requirement, but not using function pointers sort of is ;)

I would like the compiler to be able to patch in the function pointers at compile time at the call locations. But C does not have any way to do this. C++ has static polymorphism with the CRTP pattern, but that is not really friendly to use in my eyes.
Krzysiek
17 posts
Interfaces
Well, yes, it is a vtable, but I like it better this way.
I can control it and who knows what will be put in the vtable when the stupid compiler invents it. ;)

Anyway, changing call addresses in executable memory won't work with pure C, maybe you could use some assembly to achieve this.
It's tricky but if executable memory is writable it would work, why not.

Personally I wouldn't mess with such trickery unless it's something really important for some reason.
Ginger Bill
222 posts / 3 projects
I am ginger thus have no soul.
Interfaces
I use Go(lang) a lot at work and Go interfaces can be useful. In the io package, there are a few very useful interfaces: Reader, Writer, ReadWriter, WriterAt, WriterTo, ReaderAt, ReaderFrom.

Interfaces are implicit so all you have to do is implement the functions for that type and it will automatically behave as that interface. I don't use interfaces that often as I usually just use structures and functions for most things but they are useful when you need a generic function.

I do believe that they are implemented as vtables internally which can be a problem.

I know that in C++17, they will/might implement concepts which act very similar but I do not know if they will solve it. I do not know how C++17 concepts are implemented nor have I ever had the chance to use them; so I cannot comment.
Andrew Bromage
183 posts / 1 project
Research engineer, resident maths nerd (Erdős number 3).
Interfaces
gingerBill
I know that in C++17, they will/might implement concepts which act very similar but I do not know if they will solve it.

Concepts are like interfaces for templates. When you say:

1
std::vector<some_type> foo;


Then vector expects certain things to be true of some_type. In particular, it expects it to be "assignable" (i.e. the assignment operator does the obvious thing). Other containers might require other properties, such as std::map requiring its "key" type to have a working less-than operator which behaves in the way that you'd expect less-than to behave (in STL-speak, it's a "strict weak ordering").

I'm not sure what the proposal says this week. However, the vibe of previous proposals was that it was to improve error messages and catch template misuse early. For a correct program, you could remove the concept declarations and everything would work the same.
Andrew Chronister
194 posts / 1 project
Developer, administrator, and style wrangler
Interfaces
Edited by Andrew Chronister on
Stepanov and Rose's book From Mathematics to Generic Programming has this to say about concepts:
----
A concept can be viewed as a set of requirements on types, or as a predicate that tests whether types meet those requirements. The requirements concern
  • The operations the types must provide
  • Their semantics
  • Their time/space complexity
A type is said to satisfy a concept if it meets these requirements.
----

I don't know how much of that the C++17 implementation is going for.

That book makes heavy used of templates to illustrate its points about generic programming, but honestly I wouldn't want to be the one to debug any of the code written in the way they do their examples. I can't imagine the error messages.