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

111 Views Asked by At

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.

2

There are 2 best solutions below

1
zdan On BEST ANSWER

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);
0
TooZni On

You can use full specialization to get a member corresponding to a parameterized type.

struct Tile {
    //...

    template<>
    inline ETerrainType memberOfType<ETerrainType>() const
    {
        return TerrainType;
    }

    template<>
    inline EBuildingType memberOfType<EBuildingType>() const
    {
        return BuildingType;
    }

    template<>
    inline EDecorationType memberOfType<EDecorationType>() const
    {
        return DecorationType;
    }
}

Or in C++20 you can use if constexpr

struct Tile {
    //...

    template<typename T>
    inline T memberOfType() const {
        if constexpr (std::is_same_v<T, ETerrainType>) {
            return TerrainType;
        }
        else if constexpr (std::is_same_v<T, EBuildingType>) {
            return BuildingType;
        }
        else {
            return DecorationType;
        }
    }
}

And both can be used with a bog-standard function template:

template <typename T>
void Func() {
    // ...
    DoSomething(static_cast<uint16_t>(tile.memberOfType<T>()));
}

Keep in mind for this second option that if you accidentally call tile.memberOfType<FooType> or something then you'll get a weird error aboutEDecorationType not being implicitly convertible to FooType, instead of just being told the specialization for FooType doesn't exist. Though that could solved with a concept that only accepts the wanted types.