Creating Multiple instances of one object

So I am playing a game called Factorio and I have a bunch of storage chests for items and it got me wondering how that's implemented. My guess is there's a storage class or something like that and 1 storage chest is Instantiated so I can have multiple of the same storage container but they all contain different items. How would I go about that?

A few guesses from myself would be to store the Storage Chests in a vector and add a new one to the vector each time I create a new one, or use a smart pointer and allocate a new one on the heap for each instance?

Here's what I have so far, I just put something quick together to quickly test.

What should I do from here?

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

using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::unique_ptr;
using std::make_unique;

class Item
{
    public:
        Item(const string& name): mName(name)
        {}

        string GetName() const { return mName; }

    private:
        string mName{};
};

class Container
{
    public:
        Container(const string& name, int maxStorageSlots): mName(name), mMaxStorageSlots(maxStorageSlots)
        {}

        void Insert(Item& item)
        {
            mStorage.push_back(item);
        }

        void Remove()
        {

        }

        void Open()
        {
            if (mStorage.size() == 0)
            {
                cout << "This container is empty" << endl;
            }
            else
            {
				for (auto& i : mStorage)
                {
                    cout << i.GetName() << endl;
                }
            }
        }

    private:
        string mName{"Uninstantiated Container"};
        const int mMaxStorageSlots{ 1 };
        vector<Item> mStorage{};
};

int main()
{
    Item Iron("Iron");
    Item Copper("Copper");

    Container StandardStorageChest("Standard Storage Chest", 15);
    Container LargeStorageChest("Large Storage Chest", 25);

	cout << "\nStandard Storage Chest Contents\n" << endl;

    StandardStorageChest.Insert(Iron);
    StandardStorageChest.Insert(Copper);

    StandardStorageChest.Open();

    cout << "\nLarge Storage Chest Contents\n" << endl;

	LargeStorageChest.Insert(Iron);
    LargeStorageChest.Insert(Copper);

    LargeStorageChest.Open();
}
Last edited on
Hi Chay

Your code looks OK except for a couple of nit picking things :+) So, well done !!

I guess your code was to demonstrate multiple containers, so that might be the reason some of these things aren't in your code.

The Item class could be a struct, because:

1. There is a get and set function for the member variable;
2. Item is part of a private vector in the container, so there are no access worries (it's encapsulated in the vector);
3. There is no invariant. An invariant is a condition that must be upheld at all times. Prefer to use a simple public struct when there are no invariants, and a normal class with private variables when there is an invariant.

The Container class does have an invariant: maxStorageSlots There should be error checking on that - what if it is negative or some huge number? Presumably it should be less for the standard container and larger for the large container.

The Container class does not make use of the maxStorageSlots value. The ctor could use the reserve function to make sure the vector has maxStorageSlots places reserved.

One could write an ostream<< function to output the values in the Container.

std::endl is really slow, try to avoid it :+)
Also mMaxStorageSlots could be of type size_t as it will never be < 0 (hopefully!)


and to be really picky, GetName() could be defined as [[nodiscard]] (so that if used it's return value must be used).

Open() could usefully return a bool to indicate whether succeeded or not.

L49 i can be const ref.

L22 the {} is not needed as std::string has a default constructor.

L31 item can be const ref
L33 can use .emplace_back() rather than .push(back)

As class Container doesn't have a default constructor, there's no point specifying init values for mName and mMaxStorageSlots L57 58.

L59 doesn't need the terminating {}

Possibly:

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

using std::cout;
using std::vector;
using std::string;
using std::ostream;

class Item {
public:
	Item(const string& name) : mName(name) {}

	[[nodiscard]]
	string GetName() const { return mName; }

private:
	const string mName;
};

class Container {
public:
	Container(const string& name, size_t maxStorageSlots) : mName(name), mMaxStorageSlots(maxStorageSlots) {
		mStorage.reserve(mMaxStorageSlots);
	}

	void Insert(const Item& item) { mStorage.emplace_back(item); }
	void Remove() { }

	bool Open() const {
		if (mStorage.size() == 0)
			return (cout << "This container is empty\n"), false;

		return !!(cout << *this << '\n');
	}

	friend ostream& operator<<(ostream& os, const Container& cont) {
		for (const auto& i : cont.mStorage)
			os << i.GetName() << '\n';

		return os;
	}

private:
	const string mName;
	const size_t mMaxStorageSlots {};
	vector<Item> mStorage;
};

int main()
{
	const Item Iron("Iron");
	const Item Copper("Copper");

	Container StandardStorageChest("Standard Storage Chest", 15);
	Container LargeStorageChest("Large Storage Chest", 25);

	cout << "\nStandard Storage Chest Contents\n";

	StandardStorageChest.Insert(Iron);
	StandardStorageChest.Insert(Copper);

	StandardStorageChest.Open();

	cout << "\nLarge Storage Chest Contents\n";

	LargeStorageChest.Insert(Iron);
	LargeStorageChest.Insert(Copper);

	LargeStorageChest.Open();
}

Last edited on
One more thing ! Super pedantic, defensive programming ;+)

seeplus wrote:
Also mMaxStorageSlots could be of type size_t as it will never be < 0 (hopefully!)


Here's something I do in this situation:

1
2
3
4
5
6
7
8
void f(std::size_t a) {
    std::cout << "a is " << a << "\n";
}

int z  = -1; // bad, should have been std::size_t

//oops
f({z}); // compiler warns about narrowing because of brace initialisation 


Another way of doing things:

Create a Validation class which is privately inherited into the class in question. The class ctor calls functions from the Validation class to verify that all values are within appropriate ranges and are ok. These functions can also be called to enforce invariants every time a value is initialised, changed, and when values are returned. There is a boost contract programming library, to help with this:

https://www.boost.org/doc/libs/1_76_0/libs/contract/doc/html/index.html
In C++20, you can do things like:

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

using std::cout;
using std::vector;
using std::string;
using std::ostream;

class Item {
public:
	Item(const string& name) : mName(name) {}

	[[nodiscard]]
	string GetName() const { return mName; }

private:
	const string mName;
};

class Container {
public:
	Container(const string& name, std::unsigned_integral auto maxStorageSlots) : mName(name), mMaxStorageSlots(maxStorageSlots) {
		mStorage.reserve(mMaxStorageSlots);
	}

	void Insert(const Item& item) { mStorage.emplace_back(item); }
	void Remove() { }

	bool Open() const {
		if (mStorage.size() == 0)
			return (cout << "This container is empty\n"), false;

		return !!(cout << *this << '\n');
	}

	friend ostream& operator<<(ostream& os, const Container& cont) {
		for (const auto& i : cont.mStorage)
			os << i.GetName() << '\n';

		return os;
	}

private:
	const string mName;
	const size_t mMaxStorageSlots {};
	vector<Item> mStorage;
};

int main()
{
	const Item Iron("Iron");
	const Item Copper("Copper");

	Container StandardStorageChest("Standard Storage Chest", 15U);
	Container LargeStorageChest("Large Storage Chest", 25U);
	//Container Bad("Imaginary Chest", -1);	// Doesn't compile as signed

	cout << "\nStandard Storage Chest Contents\n";

	StandardStorageChest.Insert(Iron);
	StandardStorageChest.Insert(Copper);

	StandardStorageChest.Open();

	cout << "\nLarge Storage Chest Contents\n";

	LargeStorageChest.Insert(Iron);
	LargeStorageChest.Insert(Copper);

	LargeStorageChest.Open();
}


Note L58

@seeplus

Excellent, thanks for that :+)

TheIdeasMan.ThingsLearnt += 2; // use of auto in that context as well

Thanks for the input, thats stuff I didnt know so thats a plus, as for the Item class, I just made something simple. If I were to fully flesh it out then It would be a full blown class, I could have made it a struct but if this were a real program it would have been a class.

But what I needed help with is if lets say I had a container class and needed multiple instances of one type of the same container type, lets say in this game the player makes 3 Standard Containers, how do I make it so that when the player places all three down that when they place something in one container, it doesnt show up in the other two? so each container is instanced and doesnt share its contents?

In my OP I can have multiple containers, but those are hard coded, in factorio, you can make as many chests as you want, would I use a pointer and/or a vector to create instances at runtime? Or maybe push back a chest to the heap?
Last edited on
if you have a 'box' class, and a container (heh...) of those (like a vector) each with its own internal container (like vector) of thingys, they are distinct by nature if you did not do something to make them share storage (like a static keyword, or a pointer to passed in memory).

if you want both (a magic game chest that every town you go into, it has the same items) and a normal chest (has its own inventory, no other chest can access it) then you have options ... do you ever need to do both?

yes, a container (vector) of your 'box' objects is what you need from the sound of it.
The issue is whether a Chest is a view to data (Inventory). There could be multiple views (Chests) to same data, in which case they all show the same thing (Items).

The nature of the Chest can thus dictate what a "put into Chest" actually does.


Note though that even if each Chest is separate, with its own unique inventory, there is still possiblity to see same items via multiple chests, if you don't actually put items (data) in them, but just views to items.


A pointer is more or less a "view". Kind of. Depends on how you use it.
In my OP I can have multiple containers, but those are hard coded, in factorio, you can make as many chests as you want, would I use a pointer and/or a vector to create instances at runtime? Or maybe push back a chest to the heap?
Well, the Factorio map is a grid, so most likely it looks something like
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
class Item{
public:
    virtual ~Item(){};
    virtual const std::string &name() = 0;
    //Other pure virtual functions...
};

class Chest : public Item{
protected:
    //IIRC chests are Diablo-like inventories, but this is good enough for this example.
    std::vector<std::unique_ptr<Item>> contents;
};

typedef std::pair<int, int> Location;

class Map{
    std::map<Location, std::unique_ptr<Item>> placed_items;
public:
    bool place_item(const Location &l, std::unique_ptr<Item> &item){
        auto &previous = this->placed_items[l];
        if (previous)
            //Can't place two items in the same cell.
            return false;
        previous = std::move(item);
        return true;
    }
};

int main(){
    //initialization
    
    //game loop:
    while (true){
        handle_events();
        update_state();
        draw();
    }
}

void handle_events(){
    while (!event_queue.empty()){
        auto event = event_queue.pop();
        //...
        if (event.is_mouse_release()){
            if (player_is_dragging_item() && event.is_on_map()){
                Location loc = event.get_map_location();
                std::unique_ptr<Item> &item = player.inventory.get_item_being_dragged();
                if (!map.place_item(loc, item)){
                    //Show the player an error.
                }
                //Otherwise the std::move() removed the item from the inventory already.
            }
        }
    }
}

As for where the instances originally come from, IIRC the player has to give a craft command that creates them somwhere in the inventory, that might look like
1
2
3
4
5
6
7
void craft_item(ItemClass klass, std::vector<std::unique_ptr<Item> *> &raw_materials){
    std::unique_ptr<Item> new_item = item_constructors[klass].construct();
    //remove materials from inventory
    for (auto rm : raw_materials)
        rm->reset();
    player.inventory.add_wherever(std::move(new_item));
}
Interesting thank you!
Topic archived. No new replies allowed.