Using std::variant to reduce getter and setter clutter

I'm experimenting with new ways to reduce code bloat, and one of my biggest gripes is always having to create two different functions for one variable, so my solution was to use std::variant to create one function for getters, and do something similar for setters. This approach reduces the overall code, now ALL variables have only two functions for getting and setting, and the functions take an enum so the user cannot mess up the variable name. I was able to remove
12 functions overall from the code, and if there's any more variables added, they can easily be setup in the two functions for use. I'm just curious as to what others think of this approach. I plan on refining it and making it even more intuitive, but this is my conceptual draft.

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#include <iostream>
#include <print>
#include <string>
#include <vector>
#include <variant>
#include <cctype>

class Tile
{
public:
    Tile() = default;

    enum class VariableName
    {
        BaseLayerTile,
        CharacterTile,
        HasCollision
    };

    using VariableType = std::variant<char, bool>;

    VariableType GetVariable(VariableName variable) const
    {
        switch (variable)
        {
        case VariableName::BaseLayerTile: return mBaseLayerTile;
        case VariableName::CharacterTile: return mCharacterLayerTile;
        case VariableName::HasCollision: return mHasCollision;
        }

        return {}; // Should never reach here
    }

    void SetVariable(VariableName variable, char newTile = '\0', bool newState = false)
    {
        switch (variable)
        {
        case VariableName::BaseLayerTile:
            mBaseLayerTile = newTile;
            break;

        case VariableName::CharacterTile:
            mCharacterLayerTile = newTile;
            break;

        case VariableName::HasCollision:
            mHasCollision = newState;
            break;
        }
    }

    void ClearCharacterLayer()
    {
        mCharacterLayerTile = '\0'; // Clears the character after rendering
    }

private:
    char mBaseLayerTile{ '.' };
    char mCharacterLayerTile{ '\0' };
    bool mHasCollision{ false };
};

class Map
{
public:
    Map(int height, int width, char tile)
        : mHeight(height), mWidth(width), mTile(tile), mMap(mHeight, std::vector<Tile>(mWidth, Tile()))
    {
    }

    enum class VariableName
    {
        Height,
        Width,
        Tile
    };

    using VariableType = std::variant<int, char>;

    VariableType GetVariable(VariableName variable) const
    {
        switch (variable)
        {
        case VariableName::Height: return mHeight;
        case VariableName::Width: return mWidth;
        case VariableName::Tile: return mTile;
        }

        return {}; // Should never reach here
    }

    void Draw()
    {
        for (int y = 0; y < mHeight; ++y)
        {
            for (int x = 0; x < mWidth; ++x)
            {
                Tile& tile = mMap[y][x];

                if (std::get<char>(tile.GetVariable(Tile::VariableName::CharacterTile)) != '\0')
                {
                    std::print("{} ", std::get<char>(tile.GetVariable(Tile::VariableName::CharacterTile)));
                }
                else if (std::get<bool>(tile.GetVariable(Tile::VariableName::HasCollision)))
                {
                    std::print("X ");
                }
                else
                {
                    std::print("{} ", std::get<char>(tile.GetVariable(Tile::VariableName::BaseLayerTile)));
                }
            }
            std::print("\n");
        }

        // Clear character layer after drawing so it doesn't remain permanently
        for (auto& row : mMap)
        {
            for (auto& tile : row)
            {
                tile.ClearCharacterLayer();
            }
        }
    }

    void SetCharacterAt(int y, int x, char character)
    {
        if (IsInBounds(y, x))
        {
            mMap[y][x].SetVariable(Tile::VariableName::CharacterTile, character);
        }
    }

    void SetCollision(int y, int x, bool hasCollision)
    {
        if (IsInBounds(y, x))
        {
            mMap[y][x].SetVariable(Tile::VariableName::HasCollision, {}, hasCollision);
        }
    }

    bool CheckCollision(int y, int x) const
    {
        return IsInBounds(y, x) && std::get<bool>(mMap[y][x].GetVariable(Tile::VariableName::HasCollision));
    }

    bool IsInBounds(int y, int x) const
    {
        return y >= 0 && y < mHeight && x >= 0 && x < mWidth;
    }

private:
    int mHeight{};
    int mWidth{};
    char mTile{};

    std::vector<std::vector<Tile>> mMap;
};

class Character
{
public:
    Character(char sprite, int startY, int startX)
        : mSprite(sprite), mPositionY(startY), mPositionX(startX)
    {
    }

    void Move(Map& map, char direction)
    {
        int nextY = mPositionY;
        int nextX = mPositionX;

        switch (direction)
        {
        case 'w': --nextY; break;
        case 's': ++nextY; break;
        case 'a': --nextX; break;
        case 'd': ++nextX; break;
        default:
            std::print("Invalid input!\n");
            return;
        }

        if (!map.IsInBounds(nextY, nextX))
        {
            std::print("Can't move out of bounds!\n");
            return;
        }

        if (map.CheckCollision(nextY, nextX))
        {
            std::print("You can't move there! It's blocked.\n");
            return;
        }

        mPositionY = nextY;
        mPositionX = nextX;
    }

    void PlaceOnMap(Map& map)
    {
        map.SetCharacterAt(mPositionY, mPositionX, mSprite);
    }

private:
    int mPositionY{};
    int mPositionX{};
    char mSprite{};
};

int main()
{
    Map test(10, 10, '.');
    Character player('O', 4, 4);

    test.SetCollision(5, 5, true); // Set a wall at (5,5)
    test.SetCollision(2, 2, true); // Another collision tile for testing

    bool gameOver{ false };

    while (!gameOver)
    {
        player.PlaceOnMap(test);
        test.Draw();

        std::print("\n{}\n\n", "Move Where?");
        std::print("\n{}\n", "W). Up");
        std::print("{}\n", "S). Down");
        std::print("{}\n", "A). Left");
        std::print("{}\n", "D). Right");

        char input{};
        std::cin >> input;

        input = std::tolower(input);
        player.Move(test, input);
    }
}
Last edited on
Are you sure that you need setter and getter functions for those variables in the first place?
I have always felt that getters and setters were just that: bloat. There is a place for them, if they DO something ... eg if they do validation and interaction with the user. And there is a need for consistency, so if your project/team/whatever is using the darn things, then you should conform to the pattern. But making something private for the sake of making it private and then exposing it via a function so its no longer really private in truth, but kinda sorta because direct access was banned... is academic eggheaderry at its finest: it looks great in a 10 line program in a textbook and it adds thousands of lines to a large program for no reason.

So basically my approach is, if it has a setter, its should be a public variable and will be one unless there is a compelling reason (like keeping my job) behind doing it any other way.

that said, doing something weird like this may be worse than just going with the expected norm. Now you have something strange going on that no one else is doing, and unless you are a trend setter who is writing a book or have a blog with a large following, no one else is going to do it this way and everyone else is gonna be mad about it: you have managed to find a way to piss off both the people that like getters and setters AND the people that don't with this one. Its fine if its just you and yourself with your code, but you don't want to go down this road on a team.

there may be a microscopic (but nonzero) performance cost to your approach as well. To be fair, any getter/setter that does anything more than an assignment may also have a cost, and if its not spammed in a loop, its probably of no importance.

And finally, there are tools that generate this kind of crap for you, so at least you don't have to TYPE all the getters and setters yourself. But as above, they won't do it in your weird special way, they do it like everyone else.
Last edited on
I think you (OP) are confusing your human-readable code with what actually gets compiled as code, and makes things both more complicated and more obfuscated than need be (causing bugs!).


Big & Complex Function Is Bad. Small Functions Are Good.

In your first example (Tile), you have simply offloaded three very simple functions into a single more complicated function. Not only does it all compile down to do the same thing via a function call, but the function call is slower due to the extra convolution. (You left out per-field verification, which also belongs in a setter function.) Code such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Tile
{
public:
    // Getters
    char GetBaseLayerTile()      const { return mBaseLayerTile; }
    char GetCharacterLayerTile() const { return mCharacterLayerTile; }
    bool GetHasCollision()       const { return mHasCollision; }

    // Setters
    void SetBaseLayerTile     ( char x ) { ... };
    void SetCharacterLevelTile( char x ) { ... };
    void SetHasCollision      ( bool x ) { ... };

private:
    char mBaseLayerTile;
    char mCharacterLayerTile;
    bool HasCollision;
};

...is significantly shorter in both human code AND in compiled code, is instantly readable, and self-corrects for type errors at compilation! For example:

 
some_tile.SetBaseLayerTile( SOME_TILE_CONSTANT );

There is absolutely no confusion about what is going on, and you can absolutely verify that the type of the argument is correct.

While C++ does provide ways to do things with visitor patterns and the like, it all comes down to selecting and calling a function that does something. IMO, anything that makes that more obtuse and unwieldy in code is Not A Good Thing™.


Personal Preference: Chain Yer Setters

I personally like to make setters chainable:

1
2
3
4
class Tile
{
...
    Tile & SetBaseLayerTile( char x ) { ...; return *this; }
1
2
3
some_tile
    .SetBaseLayerTile( SOME_TILE_CONSTANT )
    .SetCharacterLayerTile( SOME_TILE_CONSTANT );

Some people think this is morally equivalent to stealing candy from babies, though. JSYK.


KIS(S)

As both jonnin and mbozzi make note of above: for simple fields, using setters and getters is total overkill. The only reasons you should do it are:

  • Setting (or getting!) a field requires some kind of verification or other processing to occur at the time of access.
  • You are implementing an interface of some kind.

Otherwise just make the field public. (But if you should have either both accessors or neither.)


Field Types Should Be Constrained

Having a type of char is the same as saying any old integer will do. If the values your field may have are constrained in any way, then your field type itself should be constrained, such as by making it an enumerated type or by making it a small class.

Again, this is not a hard-and-fast rule. Use good judgement: what will it cost in terms of development and maintenance? If a simple integer will do, even though not all integer values are valid, will an invalid value be a real problem? If not, then leave it be. If yes, make a setter or make a type that cannot have invalid values.


Properties

Ultimately what you are trying to do is create a property class for C++. This has been attempted many, many, many! times.

I have even made a few property class types myself. (Some of them even worked! kind of).

The problem is that C++ is not really designed to work with data property types. It is not part of the language. And attempts to graft it in are a waste of your time and energy.


Just write a little function.
That's unfortunate haha, I love C++ but damn, sometime you have to do a LOT just accomplish very little and that's annoying, they really need to find a way to streamline things without losing the power and flexibility C++ offers. However with that said I do not believe a solution doesn't exist, I just believe no ones found it yet. It's probably something that's going to take a brilliant mind to figure out, and that definitely is not me haha.

I was always taught to make variables private and then expose them through functions, and I can see that being valid if youre doing a check to make sure the variable is valid, say for exampel you're setting a health value

1
2
3
4
5
6
7
    void SetHealth(int value)
    {
        if (value > 0)
        {
            mHealth = (value > 100) ? 100 : value;
        }
    }


I think this is completely valid. but something like this, which is what you mentioned in your post:

1
2
3
4
void SetTile(char newTile)
{
    mTile = newTile;
}


Probably not necessary, but It just feels wrong to make it public cause I was always taught not to. I just don't do much error checking cause im lazy haha
One of the reasons for using getters/setters is to hide the 'implementation' so that how the setter/getter works is hidden from the caller and can change without the caller having to change. The caller effectively treats the class as a 'black box' which performs the required function - without having to know how the function is performed. This makes it much easier later to change a class 'how' without impacting the caller. This becomes especially important if the class is part of a library used by others - although it can become important if your own class comes to be used by multiple programs.
seeplus wrote:
(good stuff)

...which is basically the definition of an interface. The other consequence is that, due to the fact that the compiler is smarter than you (and unless you do something really weird to prevent it), those simple little function calls often get compiled into a direct assignment anyway.


That's unfortunate haha, I love C++ but damn, sometime you have to do a LOT just accomplish very little...

It is already doing very little. Even if the small function call is not elided, it is very, very tiny and does nothing more than what is necessary.

Going about it differently not only causes a lot of grief on your end, but generally makes the actual compiled code larger and slower as well.


However with that said I do not believe a solution doesn't exist, I just believe no ones found it yet.

The solution lies in modifying the language, and has been hashed by the committee before. It isn’t going to happen.

Properties, while pretty, are a pain in the nose. Object Pascal has properties (and I like them!) but implementing them are a bit of a nightmare. You are simply unaware of the edge cases that occur when you try to make things that aren’t a variable look and act like a variable.


I was always taught to make variables private and then expose them through functions

Beware always/never rules.

More important than always hiding fields is the interface to the class. There is absolutely nothing wrong with making a public field.


Now I’m gonna go look at all my old property classes and play around with them. Thanks a lot. LOL



EDIT: BTW, don’t be discouraged from playing around with the language. Property classes in general are a bad idea, but for specific use cases they can be very handy. And fun!

Just wanted to make sure you know we aren’t bashing you.
Last edited on
C++ might not get properties, but it might get reflection in 2026.

What Ch1156 made was a way to switch over the member variables in a class. Reflection would provide a way to automate this process.

Sadly, I haven't been paying attention to C++'s evolution recently, so I can't do any sample code today. :(
I stopped caring at C++17.
Yes, basically the same story for me.

Currently the C++ committee does piecemeal language design. I think the turning point was around 2015, when it became trendy to add WG21 contributions to one's resume, and the committee grew tenfold in size.

If the committee hadn't grown so much, and had listened more closely to Bjarne, the language would be in a much better state.
Last edited on
It's all good, its fun to experiment and see what can be done with it. I enjoy doing weird stuff like that sometimes haha
Me too. I’m actually a big fan of RLM (radical language modification).

C++ does not make that easy, though.
"Properties, while pretty, are a pain in the nose. Object Pascal has properties (and I like them!) but implementing them are a bit of a nightmare. You are simply unaware of the edge cases that occur when you try to make things that aren’t a variable look and act like a variable."
--------------

you can do this, if you want to screw with it. You can make a class that behaves just like a variable; my first take on a matrix class (long before eig existed) was little more than that -- it did pretty much what an integer could do (assign, print, +-*/, little more). I also created a monstrosity that could be either a double or a complex<double> and it also behaved exactly like a variable of its type .. it was an afterthought bandaid but it worked for that, allowing complex to be used in a library written for doubles, with only a couple of modifications to shoehorn it in.

And, yea, you get some really weird edge cases. I ran into (remember, this was long ago and some of these have been solved by additions to the language)
- order of operations problems
- expression problems (eg a+b+c type expressions could fail to compile due to result of a+b not being a tangible variable that could be added to, it had to be rewritten to (a+b).operator+(c) or something screwball)
and a few other odds and ends that made total sense when they happened but were still beyond my ability to predict/expect doing it the first time and working through it all. I went in thinking it would be simple, and it blew up pretty quickly if used outside of its original design capabilities.
Remember that properties are part of the language in Object Pascal, and the difficulties implementing them there (which would translate to C++ as well) are the ones to which I refer.

IIRC, the main grief comes from needing to treat the property as a reference —which can’t be done without significant overhead — and even though such use is forbidden it is a surprise to users, often enough requiring clunky workarounds or (worse yet) a redesign.


Were the language to support variable traces (like Tcl, for example) then properties could be implemented easily. The consequence being that we give up fast and unencumbered direct variable referencing — something that no one wants (and will frankly never happen).
That too is about "interface". On one side is user, who wants to use the language. On the other side is the implementation of the compiler and libraries.

Didn't first C++ standard have features that nobody really implemented and hence nobody used either?


@OP: You are the main user and the implementer of class Tile? How long do you expect to agree with yourself that the interface of Tile is optimal for both sides?
Ch1156 wrote:
Probably not necessary, but It just feels wrong to make it public cause I was always taught not to. I just don't do much error checking cause im lazy haha


I guess that's ok if you have small programs for your own use, but in the real world, one has to try to make them idiot proof - as much as possible. I know this sounds like a drag, but there is a certain amount of satisfaction in realising that one has made a program fairly bullet proof, even if it is only a small program.

One of the biggest things is that every variable has a practical range of values that are valid. For example, say you want to simulate firing a rifle, you have the muzzle velocity for the round you have; and you have a fancy calculation that takes into account the aerodynamic drag on the bullet; and you have the start coordinates. The other variables you need are the compass bearing for where the rifle is pointing, and an elevation angle above the horizontal. What happens if the user enters 1000 for each of these variables? Maybe they forgot the decimal point and meant either: 1.000, or 10.00, or 100.0. Because of the way that the sine and cosine functions work, the actual angle will be 1000 modulus 360 = 280, and this will give spectacularly bad results. Much better to check that values are 0.0 or greater, and strictly less than 360.0

I like to use strong types, my motivation is as follows: One could write all kinds of applications, like a Mars space mission, using mainly one type throughout: the type double. There would be nothing wrong with this, it could work just fine. But it would be much less error prone to have a different type for every type of variable that you have. For example an X ordinate is a different thing to a Y ordinate, so they should have different types. So I create a base class named Strong_t . It has a constructor that takes one double value, the body of which does validation on the input value. It also has an operator()() which returns the value. And it has an operator()(double new_value) which sets the value and does validation also. The class only stores one value, the variable value which is protected. I then inherit from this Strong_t class and override the operators, or add more operators as required.

Now this means we can have much better interfaces, and we can delete operators that don't make sense, for example it makes no sense to add an X ordinate to a Y ordinate, so delete this operator.

Some other things to realise about interfaces, are these questions:

1. Can the variable be const or constexpr ? Obviously won't need setters for those, set upon construction.

2. Can we write an interface that works on the whole object, not individually on it's members? For example say we have a Triangle object, and we want to Rotate, Move, Stretch, and Scale it. We don't want to call a setter for each of the 3 member points of the triangle to do a Rotation, we just have 1 rotation function which alters all 3 points. Use a similar idea for Move Stretch and Scale. If those are the only valid operations for the class, then no setters or getters are required at all. So ask yourself: What do I need to be able to do with this object?

3. Will it be alright to have a struct with everything public, so long as that struct is a private member of some other class, and isn't directly exposed in an interface? The answer is yes. Think of a Point struct, it can be a private member of all kinds of classes.
I hope this helps :+)
Registered users can post here. Sign in or register to post.