Best way to modify a variable in a class

Pages: 1234
So when i add characters to the vector, its actually modifying the Character objects in the vector itself, allowing me to do things like level up attacks, or change a name etc, correct?


In my code above, the main vector is a vector of pointer to character. So any changes via the vector is via a pointer to the actual character (orc, rat etc) and hence changes the actual character.

The character has a vector of attack (copy). This means that each character now has it's own attack portfolio and changes to a character attack is only applied to that character attack and not to any other character attack. The defined attacks (punch, scratch etc) are now treated as only defaults for that attack and hence can be const as they are now never changed.
ahh ok, that makes sense. So by making the vector a vector of Character pointers, were modifying the actual characters themselves, and since each character has its own attack list, were modifying the copy of each attack that we gave that particular character. we dont make the attack vector hold a pointer because that allows each character to have its own unique attacks. Interesting. I learned something new today.

Lets say we did want the enemies level to scale with the player like it does in some games, but everything else didnt, just the characters level, we would just make the int mLevel{}; member variable a pointer? or a pointer to that variable?
Last edited on
hmm. If you want to scale it to the player, you would probably just have 1 level value for all characters, player included. That begs a static int variable.

I'll repeat something I said earlier. Try to make pointers a last resort. There are places where you do want or need them, but they carry a lot of baggage and more often than not some other tool will do what you want better.

here, for example, you can store the level variable "somewhere" and give all the characters a pointer or reference to it, sure. But that is clunky, since that "somewhere" makes the code weird -- is it a global? Or where does it go? Making it a static in the class keeps it where it belongs, yet shares it across instances of the class.
Last edited on
Ok, so when you say use pointers as a last resort, do you mean ALL pointers? Including smart ones or just raw pointers?

I was also thinking a static variable would work as well, unless I wanted the levels of the enemies to be random but close enough to the players, maybe a few levels higher or lower but that's hypothetical I don't plan to put that in the game.

So what about this situation here? Is it a good candidate for pointers or do I need to design it in a way that pointers shouldn't be necessary?

Cause in my 14 years of C++ programming I have never used them before.

What instances in 2D game design do you think it will be unavoidable to use pointers?
Last edited on
So what about this situation here? Is it a good candidate for pointers or do I need to design it in a way that pointers shouldn't be necessary?
Yes, pointer are not necessary here. Actually you are trying to let the object live within the stack and a container. That doesn't make sense.

For this you need the right container: A map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
    std::map<std::string, Character> character_map;

    character_map["player"] = {"Hero", 100};
    character_map["orc"] = {"Orc", 100};
...

    Attack punch("Punch", 7, 1);
...

    character_map["orc"].LearnAttack(punch);
    character_map["orc"].LearnAttack(orcSword);
...

    ShowAttacks(character_map);

    return 0;
}


What instances in 2D game design do you think it will be unavoidable to use pointers?
A pointer is necessary when you need to control the live time of an object. A container can do this for you.
When the object is shared by more than one container you need a [shared]pointer...
Ahh I see, so if I had multiple vectors with the same object in it and wanted to ensure that any modification from one container affected all instances of the object then I would use a pointer. That makes sense.

This code here runs good, I just made the return value a reference:

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
#include <iostream>
#include <vector>
#include <string>

class Attack 
{
public:
    Attack(const std::string& name, int level, int power)
        : mName(name), mLevel(level), mPower(power) 
    {}

    void IncreaseLevel() 
    {
        ++mLevel;
    }

    std::string GetName() const 
    {
        return mName;
    }

    int GetLevel() const 
    {
        return mLevel;
    }

    int GetPower() const 
    {
        return mPower;
    }

private:
    std::string mName;
    int mLevel;
    int mPower;
};

using Attacks = std::vector<Attack>;

class Character 
{
public:
    Character(const std::string& name, int health)
        : mName(name), mHealth(health) 
    {}

    void LearnAttack(const Attack& attack) 
    {
        mAttackList.push_back(attack);
    }

    Attacks& GetAttacks() 
    {
        return mAttackList;
    }

    const Attacks& GetAttacks() const 
    {
        return mAttackList;
    }

    std::string GetName() const 
    {
        return mName;
    }

    int GetHealth() const 
    {
        return mHealth;
    }

private:
    std::string mName;
    int mHealth;
    Attacks mAttackList;
};

using Characters = std::vector<Character>;

void DisplayInfo(const Characters& enemyList) 
{
    for (const auto& enemy : enemyList) 
    {
        std::cout << enemy.GetName() << '\n';
        std::cout << "- Health: " << enemy.GetHealth() << '\n';
        std::cout << "    - Attacks -\n";
        for (const auto& attack : enemy.GetAttacks()) 
        {
            std::cout << "        - " << attack.GetName() << " - Power: " << attack.GetPower()
                      << " - Level: " << attack.GetLevel() << '\n';
        }
    }
}

int main() 
{
    Character player("Player", 100);
    Character orc("Orc", 120);
    Character rat("Rat", 10);
    Character raptor("Raptor", 200);

    Attack scratch("Scratch", 1, 4);
    Attack bite("Bite", 1, 12);

    rat.LearnAttack(scratch);
    rat.LearnAttack(bite);

    raptor.LearnAttack(scratch);
    raptor.LearnAttack(bite);

    Characters enemyList{orc, rat, raptor};

    enemyList[1].GetAttacks()[0].IncreaseLevel();
    enemyList[1].GetAttacks()[0].IncreaseLevel();

    DisplayInfo(enemyList);

    return 0;
}
As for a container with the same objects that are modified, I think this example showcases that?

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
#include <iostream>
#include <vector>
#include <memory>
#include <string>

class Character 
{
public:
    Character(const std::string& name, int health)
        : mName(name), mHealth(health) 
    {}

    void IncreaseHealth(int amount) 
    {
        mHealth += amount;
    }

    void Print() const 
    {
        std::cout << mName << " - Health: " << mHealth << std::endl;
    }

private:
    std::string mName;
    int mHealth;
};

using CharacterPtr = std::shared_ptr<Character>;

void ModifyCharacters(std::vector<CharacterPtr>& characters) 
{
    for (auto& character : characters) 
    {
        character->IncreaseHealth(10);
    }
}

int main() 
{
    auto orc = std::make_shared<Character>("Orc", 100);
    auto goblin = std::make_shared<Character>("Goblin", 80);
    auto rat = std::make_shared<Character>("Rat", 50);

    std::vector<CharacterPtr> group1{orc, goblin};
    std::vector<CharacterPtr> group2{goblin, rat};

    ModifyCharacters(group1);
    ModifyCharacters(group2);

    orc->Print();     // Orc - Health: 110
    goblin->Print();  // Goblin - Health: 100
    rat->Print();     // Rat - Health: 70

    return 0;
}
enemyList still contains copies. The variable rat is not modified on line 113/114.

Cause in my 14 years of C++ programming I have never used them before.
What did you do within that 14 years?
Ahh I see, so if I had multiple vectors with the same object in it and wanted to ensure that any modification from one container affected all instances of the object then I would use a pointer. That makes sense.


1
2
3
int A {7};
int B {42};
int arr[2] {A, B};

A, B, arr[0], and arr[1] are four distinct objects.
The value of arr[0] is initially a copy of the value of A. The arr[0] and A are not the same object.

Nothing changes, if we replace int with Character. Still not same object.


1
2
3
int C {7};
int D {42};
int* prr[2] {&C, &D};

The prr[0] and C are not the same object either. Not even same type. One is a pointer and the other is int.
*(prr[0]) = 13; does modify C though.


1
2
CharacterPtr E = ...
CharacterPtr srr[2] { E };

Just like A and arr[0], the E and arr[0] are two distinct objects. Both are pointers. Both point to same Character object, just like pointer prr[0] points to C.
As a first simple refactor using maps instead of vectors and using an id to reference a player and attacks, consider:

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
#include <iostream>
#include <string>
#include <iterator>
#include <exception>
#include <map>

enum CHRT {FstChrt = 0, Hero = FstChrt, Orc, Goblin, Rat, noChrts};
enum ATTKS {FstAttk = 0, Punch = FstAttk, Scratch, Sword, Rifle, Knife, NoAttks};

class Attack {
public:
	Attack() = default;

	Attack(ATTKS attk, const std::string& name, int power, int level) : mattk(attk), mName(name), mPower(power), mLevel(level) {}

	std::string GetName() const {
		return mName;
	}

	int GetPower() const {
		return mPower;
	}

	ATTKS getId() const {
		return mattk;
	}

	void SetName(const std::string& name) {
		mName = name;
	}

	void IncrementLevel() {
		++mLevel;
	}

	int GetLevel() const {
		return mLevel;
	}

private:
	std::string mName;
	int mPower { };
	int mLevel { };
	ATTKS mattk {};
};

using Attacks = std::map<ATTKS, Attack>;

class Character {
public:
	Character() = default;

	Character(CHRT chrt, const std::string& name, int health)
		: mchrt(chrt), mName(name), mHealth(health) {}

	Attack& GetAttackById(ATTKS attck) {
		if (auto ret {mAttacks.find(attck)}; ret != mAttacks.end())
			return ret->second;

		throw std::runtime_error("Index error");
	}

	const Attack& GetAttackById(ATTKS attck) const {
		if (auto ret { mAttacks.find(attck) }; ret != mAttacks.end())
			return ret->second;

		throw std::runtime_error("Index error");
	}

	Attacks& GetAttacks() {
		return mAttacks;
	}

	const Attacks& GetAttacks() const {
		return mAttacks;
	}

	size_t numAttacks() const {
		return mAttacks.size();
	}

	void LearnAttack(Attack attack) {
		mAttacks[attack.getId()] = std::move(attack);
	}

	std::string GetName() const {
		return mName;
	}

private:
	std::string mName;
	int mHealth {};
	Attacks mAttacks {};
	CHRT mchrt {};
};

using Characters = std::map<CHRT, Character>;

void ShowAttacks(const Characters& enemyList);

int main() {
	Character player(Hero, "Hero", 100);

	Characters enemyList { {Orc, {Orc, "orc", 100}},
						{Goblin, {Goblin, "Goblin", 70}},
						{Rat, {Rat, "Rat", 10}} };

	// Default values.
	const Attack punch(Punch, "Punch", 7, 1);
	const Attack scratch(Scratch, "Scratch", 3, 1);
	const Attack orcSword(Sword, "Orc Sword", 21, 1);
	const Attack rifle(Rifle, "Rifle", 29, 1);
	const Attack knife(Knife, "Knife", 11, 1);

	enemyList[Orc].LearnAttack(punch);
	enemyList[Orc].LearnAttack(orcSword);

	enemyList[Goblin].LearnAttack(punch);
	enemyList[Goblin].LearnAttack(scratch);

	enemyList[Rat].LearnAttack(scratch);

	player.LearnAttack(knife);
	player.LearnAttack(rifle);
	player.LearnAttack(punch);

	player.GetAttackById(Knife).IncrementLevel();
	enemyList[Rat].GetAttackById(Scratch).IncrementLevel();

	ShowAttacks(enemyList);
}

void ShowAttacks(const Characters& enemyList) {
	for (const auto& [cid, enemy] : enemyList) {
		std::cout << "Enemy " << enemy.GetName() << ". Attack List Size: " << enemy.numAttacks() << '\n';

		if (enemy.numAttacks() == 0)
			std::cout << enemy.GetName() << " has no attacks!\n\n";
		else
			for (const auto& [attid, attk] : enemy.GetAttacks())
				std::cout << attk.GetName() << " - LVL: " << attk.GetLevel() << '\n';
	}
}


No pointers needed!
enemyList still contains copies. The variable rat is not modified on line 113/114.


But isn't that what we want? we want eh character to be modified, but each character should have its own copy of the attack so it can be independently modified since we don't want the scratch attack to be upgraded by rat and it also affects all other enemies that have scratch as well, but idk.

What did you do within that 14 years?


Not a ton honestly, its only in the last year or so that I've really started seriously learning the harder stuff and filling in gaps in my knowledge.

@keskiverto

so basically if I add a character object to a vector it creates a copy of it, and any modifications I make to the object in the vector are only made to that copy, but if I make the vector a vector of character pointers, then any modifications i make to that object in the vector are actually made to the original object itself right? And also the pointer to Character doesn't affect the actual attack object itself does it? Because the attack vector is not a pointer to attack objects, so the attacks are copied into the player and enemies so they each get their own version of the attack, is my understanding correct?

@seeplus

So why does this version work and the vector one doesn't? it's essentially the same thing right?
Last edited on
if I make the vector a vector of character pointers, then any modifications i make to that object in the vector are actually made to the original object itself right?

Not any. The object in vector is pointer. Some operations affect the pointer. The pointed to object can be affected only when one dereferences the pointer.

1
2
3
4
5
6
int A {7};
int B {42};
int* C = &A;
*C = 13;
C = &B;
std::cout << "A " << A << " B " << B << " C " << *C << '\n';

Line 3 does not affect A or B. Line 4 does affect A. Line 5 does not affect A or B.

the pointer to Character doesn't affect the actual attack object itself does it?

As above, we can modify Character object via the pointer.
Your Character object HAS-A an Attack object (or mroe). (A copy from somewhere.) You can modify the Attack within Character, just like you modify Character objects directly (without pointer).
Ok, so when you say use pointers as a last resort, do you mean ALL pointers? Including smart ones or just raw pointers?

So what about this situation here? Is it a good candidate for pointers or do I need to design it in a way that pointers shouldn't be necessary?

Cause in my 14 years of C++ programming I have never used them before.

What instances in 2D game design do you think it will be unavoidable to use pointers?


pretty much all pointers, smart or not. If you can't justify it (no other way to do it or other way is worse) then you should avoid.

I don't see any big need for pointers in anything you have right now.

14 years without pointers? Then you have skipped part of your education, as building your own trees, graphs and similar containers (which can be done without pointers, but any book/class on it will insist on them) do pretty much require pointers (or pointer like things). The algorithms used with trees and basic graphs are building blocks for other algorithms and tools that everyone in the field should probably know, like a binary search tree...

gaming of any kind is about performance, and as we already showed you, avoiding a copy of a fat object is a big deal for performance. Lets say you have 3 million big fat classes with 25 variables each, and you need to sort them quickly? How do you do that? Swapping the objects means copying all 25 items each time, yuck!! But a vector of pointers to the objects ... sorting that is just like sorting integers... one integer copied/moved... much, much faster... that is the kind of place where there isnt a better way.

what is a pointer like thing, you probably are asking? An array index! If you use a vector of things, the index into the vector... is exactly like a pointer for certain needs, like making a linked list. Give it a try as your next exercise... can you make a linked list class, once using pointers, and once using a vector's indices? Bonus points ... is there any advantage to a list using pointers over using a vector? Or is there advantage to the vector over pointers?
Last edited on
Is a thing thing, if it does not look like a thing?


You do use std::vector. It is great. How is it implemented? You don't need to know the details as long as you can use vectors. So you are not "using" pointers? True, explicitly, but std::vector is a proxy to pointer. I see vector as a pointer with syntactic sugar to make it sweet and safe.

While you may not use pointers, the code you have written for years does. You did not need to know. That was ok.


It is the explicit use of pointers that should make one ask: do I know what I'm doing? Is this the cleanest way to get the job done? Although, same question exists when using any part of any programming language.
So after reading all the comments I think I understand much better now, so if I have a program where I want to modify an object, and I am in a situation where a pointer is absolutely necessary (this program it isn't justifiable to use one, but I'll use it for illustration purposes, but let's just pretend it is) then I would make a pointer to that object, and it will modify the actual objects themselves because the pointer actually points to them. Now if I do NOT want to change what the pointer points to but want to modify the object then I would use a reference, as a reference doesn't allow the object it's pointing to to be changed but does allow modification, and if I DO want to change what it points to them I use a pointer, right? I would never use raw pointers, I would only use smart ones. But for this example I'll use raw ones for demonstration.

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
#include <iostream>
#include <vector>
#include <string>
#include <memory>

struct Person
{
	std::string name;
	int age;
	
	void SetAge(int ageToSet)
	{
		age = ageToSet;
	}
	
	int GetAge() const
	{
		return age;
	}
};

int main()
{
	Person john{"John", 20};
	Person mark{"Mark", 26};
	Person dave{"Dave", 31};
	
	std::vector<Person*> nameList{&john, &mark, &dave};
	
	nameList[0]->SetAge(45);
	
	std::cout << john.GetAge() << '\n';//Works, the pointer changed the value
}


Yes, if you did mean:
1
2
3
4
5
6
7
8
9
10
11
12
Person john{"John", 20};
Person mark{"Mark", 26};

Person* victim = &john;
victim->SetAge(45); // dereference pointer, and hence modify john
victim = &mark; // change what pointer points to, modifies the pointer
(*victim).SetAge(40); // dereference pointer (other syntax), and hence modify mark
victim = nullptr; // modify pointer to point to nobody

Person& doe = john;
doe.SetAge(50); // doe is alias for john, so modify john
// Can't make doe to refer anything else 
Last edited on
references DO change the item same as a pointer. This little program does the same thing twice, once with a reference, once with a raw pointer.


1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
using namespace std;
int main()
{
int i = 42;
int &r = i;
r ++; 
cout << i << endl;
int *p = &i;
(*p)++;
cout << i << endl;
}


references and pointers are nearly identical in some use cases.
pointers can deal with dynamic memory. References ONLY work with existing objects, but with dynamic memory, pointers can acquire new blocks of memory (that represent an object instance or variable). Pointers can ALSO point to existing items, same as a reference. The only thing that prevents you from modifying a pointed to or referred to variable is a constant reference or a copy of the original. And constant references/pointers can be circumvented by accident or intent, if the user is either unskilled or determined to do so.

line 10 in my program is another reason to shake your head at raw pointers. For anyone used to other languages, its unreadable.
Last edited on
Ok so i think i understand much better now, i now understand when to use a pointer much better, its still a bit vague but i see now when people say only use a pointer as a last resort, what they mean, and I can see exactly how useful it can be. In my code its not necessary cause we can just construct the objects in my vector and the vector no longer holds a copy. However if for some reason i absolutely needed to have the object AND I needed to pass it to a vector and I wanted the vector to modify the original then i would use a pointer, however since I do not need this functionality then we can just do 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
#include <iostream>
#include <vector>
#include <string>

class Person 
{
    public:
        Person(const std::string& name, int age) : mName(name), mAge(age) 
        {}

        std::string GetName() const
        {
            return mName;
        }

        int GetAge() const
        {
            return mAge;
        }

        void SetAge(int age)
        {
            if (age > 0 && age < 99)
            {
                mAge = age;
            }
        }

    private:
        std::string mName{};
        int mAge{};
};

int main() 
{
    std::vector<Person> people;

    // Constructing objects directly in the vector
    people.emplace_back("Alice", 30);
    people.emplace_back("Bob", 25);
    people.emplace_back("Charlie", 35);

    people[0].SetAge(45);

    for (const auto& person : people) 
    {
        std::cout << person.GetName() << " is " << person.GetAge() << " years old." << std::endl;
    }

    return 0;
}



As for the references thing, what I meant with references, is that yes they can be modified, but they can not be reseated like so:

1
2
3
4
5
int x{ 800 };
int &xRef = x;

int y{ 900 };
&x = y; //Error 
Last edited on
^^ x is an int, not a reference... but I think you said it right conceptually.
you just meant
&xRef = y; //still error

-----------------------------
this is the 'next level' and its hinted to above. Save this idea for when you feel you have a better grasp on pointers:
a vector, not all containers but specifically a vector, is almost a pointer, in terms of dynamic memory management and usage.
Lets examine that idea..
a vector: has an internal type (eg, the int of vector<int>).
a pointer: has an 'internal' type too: int * ip; the int here... the pointer's type is int!

a vector can allocate 0, 1, or N items in a solid block of memory (like an array).
a pointer also will allocate 0, 1, or N items in a solid block like an array.

pointers have a special null value when empty, vectors just have a zero size, but the key is that you can check it, for both.

vectors and pointers to blocks of items can both be accessed identically via [index] syntax. Even if its just to 1 item, [0] may be used but most coders prefer to use other notation (*p) in that case. 1 item is an array of 1 and a solid block, just a trivial version of it.

but there are differences. Most of the differences help you:
a vector can be resized and will even resize for you automatically. A pointer must be resized manually, and while it does the same work, its a great place to mess up an break your program badly: you have to allocate new space of the new size, copy the old data to the new location, and surrender the old data's memory. Vectors have other tools that pointers can only wish for, like pre-allocation of space (.reserve() ) and knowing its own sizes (both in use and allocated).

a vector will return any memory it used to the operating system when it goes out of scope or under various other conditions. A pointer will not, resulting in a memory leak.

I could go on, but this should be enough, if you understand pointers in the context of dynamic memory. The point, then, is that simply using a vector can eliminate 90% or more of pointers as dynamic memory, just as a side effect of what the class is, how it works, and what it can do. As I said above, the indexes can be used as pointer surrogates too, so int x = 3 ... vec[x] = 42... x is a lot like a pointer, and you can build graphs/trees/lists etc using those.



Last edited on
^^ x is an int, not a reference... but I think you said it right conceptually.
you just meant
&xRef = y; //still error


Yes i actually meant &xRef = y; that was a mistake on my part.

I see now. I always knew vectors used pointers under the hood to be able to do what it does, but i didnt know all that other stuff. So i've learned that i should only use a pointer if i absolutely have to and what kind of circumstances that would be, like for example if i need access to the original object, and also if i wanted to do any kind of memory allocation at runtime right?
Pages: 1234