Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Struct vs. array for multi-leveled data organization

I have some data that I need to organize. I should note that I'm new to C and C++.

I have a list of buildings, each with a quantity and array of resource rates associated with it. I need to loop through the list of buildings, multiply the quantity of each building by its resource differential, and sum them to arrive at a total resource differential.

For instance:

Ice Well: [-100 power, +50 water], n = 2

Solar Array: [+150 power], n = 2

Total: [+100 power, +100 water]

I'm confused as to how to organize my data. If I use a struct, it will be challenging to loop through, but an array will be confusing once the number of building types increases. At this point my best guess would be to couple an enum with an array to get the naming functionality of a struct with the looping functionality of an array. How should I go about doing this?

like image 700
J.T Avatar asked Dec 19 '22 12:12

J.T


1 Answers

You might want to consider an approach like this: (live example: http://ideone.com/xvYFXp)

#include <iostream>
#include <vector>
#include <memory>

class Building {
  public:
    virtual int getPower() = 0;
    virtual int getWater() = 0;
    virtual ~Building() {}
};

class IceWell : public Building {
  public:
    virtual int getPower() {return -100;}
    virtual int getWater() {return 50;}
};

class SolarArray : public Building {
  public:
    virtual int getPower() { return 150; }
    virtual int getWater() { return 0; }
};

int main()
{
    std::vector<std::shared_ptr<Building>> inventory;
    inventory.emplace_back(new IceWell);
    inventory.emplace_back(new SolarArray);
    inventory.emplace_back(new IceWell);
    inventory.emplace_back(new SolarArray);

    int total_power = 0;
    int total_water = 0;
    for (auto building : inventory)
    {
        total_power += building->getPower();
        total_water += building->getWater();
    }

    std::cout << "Total power: " << total_power << "\n";
    std::cout << "Total water: " << total_water << "\n";

    return 0;
}

The base class, Building defines the interface; essentially it lists the resources your buildings may produce or consume. The derived classes IceWell and SolarArray then implement that interface, defining the amount of a resource that one such building will source or sink. Finally, you have a vector containing some number of instances of the derived objects (held in shared pointers to handle the memory management for you!). You can loop over these and accumulate total values for each resource.

If the production / consumption values might change over time, you can hold these as (private) member variables of each derived class, and allow some mechanism to modify them. You could even have getWater() compute its value as a function of some measure of time, so the well might "dry up" as time progresses.


After posting my original answer (above), I found myself thinking about alternatives. I wrote the following in an attempt to explain some of the design options, their benefits and drawbacks, and how to go about reasoning about the design of a system like this. I hope it is helpful.


It is rare in C++ to find a "single correct solution". There are often several reasonable approaches, each of which will have its own set of relative merits and drawbacks.

When you're designing a program in C++, you will inevitably have to consider some of the possible solutions, and pick one based on its merits. Let's attempt to analyse the problem stated here...

You have some buildings, and these buildings have properties defining their resource usage. Some buildings are net producers, while other buildings are net consumers of a particular resource. This immediately identifies "resources" as an entity within the system. We might jump straight in and code up something that encapsulates our idea of net resource usage:

struct ResourceFootprint {
    int power;
    int water;
};

You can now represent different levels of resource consumption by instantiating that struct appropriately:

(Live example: http://ideone.com/ubJv8m)

int main()
{
    ResourceFootprint ice_well = {-100, +50};
    ResourceFootprint solar_array = {+150, 0};

    std::vector<ResourceFootprint> buildings;
    buildings.push_back(ice_well);
    buildings.push_back(ice_well);
    buildings.push_back(solar_array);
    buildings.push_back(solar_array);

    ResourceFootprint total = {0, 0};
    for (const ResourceFootprint& r : buildings)
    {
        total.power += r.power;
        total.water += r.water;
    }

    std::cout << "P: " << total.power << ", W: " << total.water << "\n";

    return 0;
}

This seems nice; we've packaged up the resources, they have handy names, and we can create new kinds of building just by providing the relevant values. This means we can have a building inventory, stored in a file, that might read something like:

IceWell,-100,50
SolarArray,150,0
HydroElectricPlant,150,150

All we have to do is read the file and create instances of ResourceFootprint. This seems like a reasonable solution; after all, the different kinds of resource are probably reasonably fixed, while the different kinds of building that produce and consume them could change often. You might want to add a Hospital, a GasStation, a Restaurant and a Farm.

But wait; Hospitals also consume Medicine, and produce MedicalWaste. Gas stations definitely need Oil, Restaurants will consume Food, as well as Water and Power, and Farms might produce Food, but they will require Oil, Power and Water to do so.

Now we're in a situation where the different kinds of resource have to change. The current mechanism still works out ok; we can add the resources to our struct:

struct ResourceFootprint {
    int power;
    int water;
    int oil;
    int food;
    int medicine;
    int medical_waste;
};

Things that didn't use the new resources before don't have to care (though the definition will probably need to zero out the extra resource fields), and things that use the new resources can be defined in terms of them:

    ResourceFootprint ice_well = {-100, +50, 0, 0, 0, 0};
    ResourceFootprint solar_array = {+150, 0, 0, 0, 0, 0};
    ResourceFootprint hospital = {-100, -50, 0, -50, -50, 50};
    ResourceFootprint gas_station = {-10, 0, -100, 0, 0, 0};
    ResourceFootprint restaurant = {-20, -20, 0, -100, 0, 0};
    ResourceFootprint farm = {-10, -30, -10, 200, 0, 0};

Better still, the existing code that computes total power and water usage will still work fine. We can just add the code for the other resources to it:

(Live example: http://ideone.com/rukqaz)

    ResourceFootprint total = {0, 0, 0, 0, 0, 0};
    for (const ResourceFootprint& r : buildings)
    {
        total.power += r.power;
        total.water += r.water;
        total.oil += r.oil;
        total.food += r.food;
        total.medicine += r.medicine;
        total.medical_waste += r.medical_waste;
    }

So far, so good. But what are the drawbacks of this approach?

Well, one obvious problem is that the resource footprint is just plain data. Sure, we can change the values, but we can't really do anything complicated, like have an IceWell dry up as time passes, or allow a SolarArray to produce different amounts of power depending on whether it is day or night. For this, we need the resource footprint to be computed in some way that might vary from one building type to another.

The original part of the answer explores one solution in which each building has its own type, with member functions that return the current consumption of the resource in question. As we just explored, we might need to extend our set of resources. We can do this by combining the two ideas; have a class for each building, and a struct to hold the resource usage. Each building class can then decide how to implement its resource usage.

The base class would look like this:

class Building {
    public:
        virtual ~Building() {}

        virtual ResourceFootprint currentResourceLevel() = 0;
};

We choose to return a ResourceFootprint by value (as opposed to returning a reference, or any other approach) because this allows us to trivially change the implementation. Discussion follows...

In the simplest case, a hydroelectric plant might just use a constant supply of water, and produce a constant supply of power. Here, it would keep a ResourceFootprint object as a (possibly const) member variable, and return a copy of it when asked for its resource consumption:

class HydroElectricPlant : public Building {
    public:
        HydroElectricPlant(const ResourceFootprint& r)
         : resources(r) {}

        virtual ResourceFootprint currentResourceLevel() { return resources; }

    private:
        const ResourceFootprint resources;
};

The IceWell might do something more complex:

class IceWell : public Building {
    public:
        IceWell(const ResourceFootprint& initial)
         : resources(initial) {}

        virtual ResourceFootprint currentResourceLevel() { return resources; }

        void useWater(int amount) { resources.water -= amount; }

    private:
        ResourceFootprint resources;

};

And the SolarArray might do:

class SolarArray : public Building {
    public:
        SolarArray(const ResourceFootprint& r)
         : day_resources(r), night_resources(r)
        {
            night_resources.power = 0;
        }

        virtual ResourceFootprint currentResourceLevel()
        {
            if (is_day())
            {
                return day_resources;
            }
            else
            {
                return night_resources;
            }
        }

    private:
        ResourceFootprint day_resources;
        ResourceFootprint night_resources;

};

The design now allows for:

  • Adding new kinds of resource: The code above will continue to work when new fields are added to the struct (although you may need to make some changes to the initialisation of the struct to account for those new fields).
  • Adding new kinds of building: You can write another class, inheriting from Building, and define its resource usage however you like.

What doesn't this cover?

  • Dynamic creation of building types. The first solution, where a building is defined purely in terms of the values of the ResourceFootprint fields, allowed us to create new kinds of building just by makiung a new instance of the ResourceFootprint with appropriate values. This fits nicely with systems that allow you to make custom buildings, but doesn't provide much flexibility in how those values change over time.
  • Dealing with new resource types without having to add more code. Each new resource type has a field in the struct, so you need to refer to it by name. You could just hold a std::vector whose elements are to be interpreted in a particular order (e.g. res[0] is water, res[1] is power) to achieve this goal, but you sacrifice code clarity to achieve that.
  • Countless other possibilities that have not yet occurred to me!

There are, of course, other solutions, but I hope this has provided a little insight into the kind of thought process you're going to need to adopt when designing systems in C++. It's quite likely that you'll come up with a design you are happy with, get half way through your program, then realise that you have hit a wall where your design simply will not work. That's fine, it happens. Go back and do the design phase again, armed with the knowledge of where your first one went wrong.

like image 78
Andrew Avatar answered Jan 01 '23 11:01

Andrew