Search code examples
c++templatesvectormacrospacking

Better way of writing similar functions that differ only by struct/class member variable name?


I have defined a struct and enums like this:

enum class ETerrainType : uint16_t {/*Enum types here*/};
enum class EBuildingType : uint16_t {/*Enum types here*/};
enum class EDecorationType : uint16_t {/*Enum types here*/};

struct Tile
{
    ETerrainType TerrainType;
    EBuildingType BuildingType;
    EDecorationType DecorationType;
};

Note that the member variables of the struct all have the same underlying type (uint16_t).

I have a container of tiles std::vector<Tile> Tiles;
I create a function containing a loop for each member variable like this:

void TerrainTypeFunc()
{
    for (Tile& tile : Tiles)
    {
        DoSomething(static_cast<uint16_t>(tile.TerrainType));
    }
}
void BuildingTypeFunc()
{
    for (Tile& tile : Tiles)
    {
        DoSomething(static_cast<uint16_t>(tile.BuildingType));
    }
}
void DecorationTypeFunc()
{
    for (Tile& tile : Tiles)
    {
        DoSomething(static_cast<uint16_t>(tile.DecorationType));
    }
}

It feels to me like poor practice to redefine each function when the only difference between each is the member that is accessed.

My first thought is to store the member variables as a fixed array of uint16_ts; however, I would like to take advantage of struct packing (e.g. the compiler could store these three members variables in one 64-bit word).
To my knowledge, storing it this way would not allow for packing. Plus, I think it is more readable to use the member variables by name than by index.

Another option would be to use an offset to access the variables; for example, get a pointer to TerrainType and add 1 to the pointer to get BuildingType, or add 2 to the pointer to get DecorationType.
To my knowledge, this is bad practice because it's difficult to determine what offset to use as there may or may not be struct packing in different cases. Plus, I think it is bad for readability.

A third option is to use a function which determines which parameter to use at runtime using branching. I'd prefer not to do this as I don't want to incur any runtime costs.

My question:
How can I define the three functions using less code, without incurring additional runtime costs?
I think it can be done preferably using templates, if not then with macros, but I don't know how.


Solution

  • You can use the std::invoke function to treat each data member as a callable that you pass into a common function, like this:

    template<typename F>
    void DoSomethingOn(F typeLookup)
    {
        for (Tile& tile : Tiles)
        {
            DoSomething(static_cast<uint16_t>(std::invoke(typeLookup ,tile )));
        }
    }
    

    You would call it like this:

    doSomethingOn(&Tile::TerrainType);
    doSomethingOn(&Tile::BuildingType);
    doSomethingOn(&Tile::DecorationType);