Is my understanding of pointers correct?


So I've been studying pointers and really trying to learn how to use them and how they work, and I feel like I completely understand their use cases now.

If we have large objects, like a std::vector that holds 50 million strings, or large game data like textures, videos, sounds, etc., then we need to put that stuff on the heap. If we put it on the stack, it will quickly overwhelm it and cause a stack overflow, since the stack has limited memory (~1MB per thread on Windows) and is mainly meant for function calls and small local variables.

We also need to use pointers for polymorphism, because if we store a derived class object inside a base class variable, any extra information inside the derived class will be sliced off.

For example, say we have a base Enemy class, and a derived Orc class that has a unique stealth variable. If we store an Orc as a regular Enemy, the stealth variable gets sliced off, because the base class does not know about derived-specific data.

However, if we use a pointer, it stores the full object’s memory address, which allows the base class pointer to refer to the complete derived class, preventing slicing and enabling polymorphism.

So basically pointers are just a way to be able to work with very large amounts of data without overwhelming the stack, and to be able to access the full data in memory, right?
With the STL containers, they already put their data on the heap, so no need for anyone to do it explicitly. Basically, the containers have a pointer to the beginning of the data; and there is a size variable. The variables are created using RAII, basically meaning that they are created in the constructor, and deleted in the destructor, which means they don't dangle.

There are some organisations who explicitly don't use the heap: In the OS they change the stack size to some large value, then put all of their variables on the stack. This avoids a whole range of problems associated with heap memory: heap corruption, double free, use after free etc. Use of stack memory is also much faster because the allocate function has to find a chunk of memory in the heap to use; and the free function has to do work to make that memory available. Paradoxically, it means that one has to use a container that doesn't put it's data on the heap - maybe they have written their own containers that don't use the heap? That's a lot of work, maybe the organisation is big enough to make it worthwhile?

Consider looking at the use of a memory pool. This is a really good idea that not many people use in their code. IIRC, Cubbi did a survey of developers and found that relatively few used memory pools.

With virtual polymorphism, there are 2 things: One declares a container of pointer to base class, then populate it with pointer to derived classes; because a pointer to derived is a valid pointer to base.

You should also look at references and smart pointers, and generally prefer to use them rather than raw pointers. References and smart pointers were invented to help overcome the problems with raw pointers, but there are some situations where their use is OK - such as non owning raw pointers.

References work just as well as pointers with virtual classes, except that one cannot have a container of references; use std::reference_wrapper instead.

Also consider looking at std::span and other ways of avoiding overruns on a data structures.

Be aware that there are safe and unsafe versions of things in the standard library: the safe ones do bounds checking implicitly; the unsafe ones do not. For example, looking at std::range adapters, std::views::take() safely takes only up to min(n, nums.size()) elements. I guess the unsafe ones are still ok in some situations: if one has a container with 1 million items, the coder knows a variable is in the middle somewhere, and only wants say 10 items. But then Murphy's Law says that someone will change the code in the future, and break it. And that may cause a rare problem where the requested amount of data is large enough.
Ch1156 is nevertheless correct in his idea that storing a single copy of an object and thereafter referencing it via pointers is a good and common technique when dealing with static objects and/or tight memory constraints.

Things like vectors, while using the heap, still make copies of the objects it holds (unless the vector is explicitly a vector of pointers).

+1 for smart pointers.
I'll take a look at those functions. I always prefer and use smart pointers over raw ones for sure, i always try to use the latest standard of C++ possible to keep up. I made an example program to illustrate my current understanding of pointers and polymorphism, i might make an SFML example that loads large data such as textures to illustrate as well, but im unsure. I'll also have to look into a memory pool, ive never heard of that before.

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

class Enemy
{
    public:
        Enemy(int health, const std::string& name, int attackPower)
            : mHealth(health), mName(name), mAttackPower(attackPower) {
        }
        virtual ~Enemy() = default;

        virtual void Attack() = 0;
        virtual void TakeDamage(int amount) = 0;
        virtual int GetSpecialAbility() const = 0;

        int GetHealth() const { return mHealth; }
        int GetAttackPower() const { return mAttackPower; }
        std::string GetName() const { return mName; }
        int GetMaxDefense() const { return mMaxDefense; }

    private:
        std::string mName{};
        int mAttackPower{};
        static constexpr int mMaxDefense{ 100 };

    protected:
        int mHealth{};
};

class Stegosaurus : public Enemy
{
    public:
        Stegosaurus() : Enemy(500, "Stegosaurus", 78), mArmorAbility(100) {}

        void Attack() override
        {
            std::print("{} uses Stomp!\n", GetName());
        }

        void TakeDamage(int amount) override
        {
            if (amount > 0)
            {
                int finalDamage = static_cast<int>(amount / (1 + (mArmorAbility / GetMaxDefense())));
                mHealth -= finalDamage;

                if (mHealth < 0)
                {
                    mHealth = 0;
                }
            }
        }

        int GetSpecialAbility() const override { return mArmorAbility; }

    private:
        int mArmorAbility{};
};

class Raptor : public Enemy
{
    public:
        Raptor() : Enemy(250, "Velociraptor", 46), mClawAttack(35) {}

        void Attack() override
        {
            std::print("{} slashes with its claws!\n", GetName());
        }

        void TakeDamage(int amount) override
        {
            mHealth -= amount; // Raptors don't have armor, so they take full damage
            if (mHealth < 0) mHealth = 0;
        }

        int GetSpecialAbility() const override { return mClawAttack; }

    private:
        int mClawAttack{};
};

void GetDamageTaken(std::vector<std::unique_ptr<Enemy>>& enemies, int index)
{
    if(index >= 0 && index <= enemies.size())
    {

            int initialHealth = enemies[index]->GetHealth();
            enemies[index]->TakeDamage(32);
            int damageTaken = initialHealth - enemies[index]->GetHealth();

            std::print("The {} took {} damage! Remaining Health: {}\n",
                enemies[index]->GetName(), damageTaken, enemies[index]->GetHealth());
    }
    else
    {
        std::print("Index {} is invalid, valid index range is 0-{}\n", index, enemies.size() - 1);
    }
        
}

int main()
{
    std::vector<std::unique_ptr<Enemy>> enemies;
    enemies.push_back(std::make_unique<Stegosaurus>());
    enemies.push_back(std::make_unique<Raptor>());

    GetDamageTaken(enemies, 0);
    GetDamageTaken(enemies, -1);
}
Last edited on
@Duthomhas

Yes, I agree. I should have made that clearer in my post.

@Ch1156

What I am about to say is off topic, but we were talking about getters and setters in another topic.

So this code does a few things:

1. Create some different sorts of enemies;
2. Allow each enemy to be attacked, reducing it's health;
3. Print the remaining health for each enemy.

In my mind, this code doesn't need get functions. The interface should allow attacks to take place, and allow attacks to be received. So if one had member functions for both of those and the member variables were protected in the base class, it means no get functions are required, because those functions already have access to the member variables. The printing of the health data could be part of the ReceiveAttack function; or a member function called Print(), or overload operator<<() By making GetDamageTaken a function external to the class, it forces the need for the get functions. I am saying neither of those things are necessary. You can accomplish everything with 2 or 3 interface functions, and obviate the need for 4 get functions.

The other thing about encapsulation is that things remain stable if the representation of something changes. So if health was no longer just an int, and now is a class which has the ability to warn the user when health is low, things should still work. So here it is ok because TakeDamage is a function.

Btw, you should check that the argument to the TakeDamage function is not negative.
Couple of minor points:

1) I'd have a using for type std::vector<std::unique_ptr<Enemy>> (eg using Enemies = std::vector<std::unique_ptr<Enemy>> )
2) Where a variable is defined but not changed afterwards this can be defined as a const (eg L47, L90, L92)
3) enemies parameter of GetDamageTaken() should be const
4) L87 the test should be index < enemies.size() and not <=
5) As index into the vector will always be >=0 and type of .size() is size_t, then the type of index should be size_t and not int.

Enemy class should have a member function print() (or similar). So:

1
2
3
4
5
6
7
void DisplayDamageTaken(const Enemies& enemies, size_t index)
{
    if (index < enemies.size())
        enemies[index]->print();
    else
        std::print("Index {} is invalid, valid index range is 0-{}\n", index, enemies.size() - 1);
}

So basically pointers are just a way to be able to work with very large amounts of data without overwhelming the stack, and to be able to access the full data in memory, right?


while I think you nailed the modern common use cases pretty well, pointers can do a lot of cool stuff. Here are a few of them, most of which you may never have to do in modern c++ because it does much of it for you, but not ALL of it :)

- pointers can chain data. A linked list, which is provided by the STL, could use pointers to link the head to next to next ... to last. The STL does NOT provide a tree or graph structure and you would need to use a third party library or roll your own there, so this COULD be something you do one day.

- pointers can be very like a 'reference' to data. Not just objects, but like just an integer, same as int &x. You see less of this in modern programming but from time to time data is passed to a function in a pointer or as a pointer, and it is free to be modified (the original!) by the function. This is a huge concept... the pointer can't be modified (its passed by value) but the thing pointed to CAN be. Re-read that until you have it internalized.

- pointers can have no type (a "void" pointer). You WILL use this, but may not write a lot of code that exploits the idea. Thread libraries are one of the first places you might see it, as most of those pass the arguments to the thread function via a void pointer.

- pointers can be used for iteration and iterative access of memory blocks. You see less of this today, thanks to range based for loops, but for example you can say p = &vectorvar[0]; for(stuff) *p = 0; ++p; and p will move to vectorvar[1], [2], ... I never liked this way of doing things, but you may see it in older code or if you get a C file in your c++. One place where that is useful is negative iteration. You can iterate a pointer backwards and even with a negative value, eg ptr[-10] = 42; That is normally not very attractive or useful, but for like a counting sort based algorithm... say you are dealing with signed bytes. An array of 256 values, take a pointer into the middle of it, and you can use the pointer with a signed value to count up how many times -42 appeared in your data. Its one of those things you may never do in a lifetime, but its there if you need it.

- pointers can be used for performance tuning. Say you had a big fat person object with all their person stuff like name, address, education, finances, all that kind of thing. Some annoying user comes along and wants your list of millions of people arranged by their income, but your data is sorted by last name then by first name then by location (eg city or something). You could run your big fat objects through std::sort and its not bad, maybe it takes 10 seconds to get it back out. But it would be done before your finger left the mouse if you sorted a vector of pointers to the data by the criteria you wanted, which is akin to sorting a list of integers instead of a list of fat objects.

There are more things like these where creative use of pointers goes way beyond just memory management and polymorphing.

I know what you meant, but pointers are not a 'way'. Pointers are a 'thing'. Mechanically, pointers are just special integers. Conceptually, they are a way to store an address in memory. Think of memory as a giant array/vector. A pointer is an integer that holds a location in that array. vector<int> memory; ... int ptr = 42; x = memory[ptr]; It works a lot like that, conceptually. So much so that most of what I told you up there can be done with an array, including building a linked list, using the index of each location instead of a pointer to chain the data.
Last edited on
One more thing ....

PIMPL - Pointer to implementation idiom aka Bridge Pattern - look it up :+D
+1
Those are interesting use cases as well, and im sure ill be using some of those for game development like object pools and graphs
I didnt see your posts after the last one, ill look up the bridge pattern. is there any other patterns that you know of would be useful for 2D game design? i still have to learn a bit more before i move to learn SFML better, but it would probably be good to learn before starting that as well.

There are several well-known patterns. They were originally discussed in the book 'Design Patterns' (known as the 'Gang Of Four' [gof] book as it has four authors). Although this book is now dated.
https://www.amazon.co.uk/Design-patterns-elements-reusable-object-oriented/dp/0201633612/ref=sr_1_3

Other books include:

Modern C++ Design
https://www.amazon.co.uk/Modern-Design-Generic-Programming-Patterns/dp/0201704315/ref=sr_1_1

Design Patterns in C++
https://www.amazon.co.uk/Design-Patterns-Modern-Approaches-Object-Oriented/dp/1484272943/ref=tmm_pap_swatch_0
Last edited on
While we are talking about Design Patterns, there are also anti-patterns: things one shouldn't do.

Also, try to make use of [[nodiscard]] so that function return values may not be discarded.
https://en.cppreference.com/w/cpp/language/attributes/nodiscard
I don’t think it is a good idea to blithely apply [[nodiscard]] to functions. Consider the primary purpose of the function: its result or its side effect(s)?

IMHO, only pure functions should have a [[nodiscard]] attribute attached to them.
nodiscard is great when used very carefully. It actually makes things worse when its blindly glued to a function where you truly don't want the result. A lot of OOP tools fight you tooth and nail against reusing the code, because the author had a use case in mind and yours is a little different.
Duthomhas wrote:
IMHO, only pure functions should have a [[nodiscard]] attribute attached to them.


Yes that makes sense.

Also, if one wanted a function for it's side effects, could that function have a void return type, which would obviously remove the need for [[nodiscard]]?
A lot of functions exist where you get both side effect and a result you can do something with, particularly with class methods.

Lambda calculus languages also have this feature. It is very useful!

(One of the reasons I do not like Haskell is that it tries to stuff the is/isn’t issue of pure/impure functions down your throat, so you need to do things like contaminate every function between call and output with a Monad.)
Normally. I dunno. Say you have a live update of your gps position in a user window. Its updating several times a second. You are making it go using MFC's setwindowtext function, which returns a bool for succeed/fail. You probably SHOULD check it every time to see if it failed, but if it did, so what? You are going to overwrite it faster than you can do anything meaningful. Ive been known to ignore things like that when 1) it really, truly never fails and 2) the is no real harm done and not a lot to be done about it. A lot of old frameworks and such work like that. It should have been a void and throw something, but it didn't, it tinkers with a global variable and returns a bool instead.

I can't think of any reason to keep writing stuff LIKE THAT where the subject would come up, though.
Ok groovey, that all makes sense :+D
Registered users can post here. Sign in or register to post.