Jeremy Friesner
Jeremy Friesner

Reputation: 73294

How to detect a missing string-array-item-initializer at compile-time?

Here's a simple code-pattern that keeps biting me in our codebase:

// in some_header_file.h
enum Color {
   red = 0,
   green,
   blue,
   max_item
};

// in some_other_file.cpp
static const char * color_names[max_item] = {
   "red",
   "green",
   "blue"
};

std::string get_color_name(Color c) {
   if ((c >= 0) && (c < max_item)) return color_names[c];
   return "Unknown color";
}

... the above is all well and good, until one day, some careless programmer (okay, it's usually me) comes by and inserts a new color (e.g. yellow) into the Colors enum in some_header_file.h (just before max_item), but forgets to also add the new color's string (e.g. "yellow") to the end of the color_names[max_item] array in some_other_file.cpp.

After that happens, the code compiles fine with no errors or warnings, but future calls to get_color_name(yellow) will (at best) not return the expected result or (at worst) invoke undefined behavior and maybe crash.

What I'd like is for this mistake to be caught at compile-time, so that I can avoid introducing runtime-error when updating enums. Is there a way in C++ to enforce at compile-time that the number of initializer-strings for color_names must be equal to the array's length (i.e. to max_item)?

Upvotes: 2

Views: 91

Answers (3)

Joma
Joma

Reputation: 3869

This is an alternative to your problem, but may not be applicable if you don't have control to modify the source headers where the enums are defined.

The full answer here: How to easily map c++ enums to strings

My solution with macros

More complete/efficient code. But can handle up to 170 enum values, if you need more than 170 feel free to modify this code. This code generates extensions methods for manipulate enum.

  • ToString - returns the name of enum. If another enum has the same numeric value, the returned name is the first enum with value.
  • ToIntegralString - returns the numeric value to string.
  • ToIntegral - returns the numeric value of enum. The type is the underlying type of enum.
  • Parse - convert the string to enum value, it can throws an exception.
  • Parse - convert the numeric value to enum value, the numeric value type is the same unlying type of enum, it can throws an exception.
  • GetValues - return a vector with all enun values.

Syntax EZNUM_ENUM(EnumName,Var1,Oper1,Val1,Var2,Oper2,Val2,......,Var170,Oper170,Val170)
EZNUM_ENUM_UT(EnumName,UType,Var1,Oper1,Val1,Var2,Oper2,Val2,......,Var170,Oper170,Val170)

  • EnumName - Name of enum class.
  • UType - Name of underlying type.
  • VarN - Name of enum value.
  • OperN - Assignment operator can be EQ for equals or _ for no operator.
  • ValN - Underlying type value. If OperN is _ this value is ignored.

this macro needs groups of 3 - Var,Oper,Val examples:
X,_,_ it generates X
Y,EQ,2 | it generates Y = 2
Z,_,2 | it generates Z

For example

EZNUM_ENUM(MobaGame,
        Dota2, EQ, 100,
        LeagueOfLegends, EQ, 101,
        HeroesOfTheStorm, EQ, 102,
        Smite, EQ, 103,
        Vainglory, EQ, 104,
        ArenaOfValor, EQ, 105,
        Paragon, EQ, 106,
        HeroesOfNewerth, EQ, -100)

It generates

enum class MobaGame : int
{
    Dota2 = 100,
    LeagueOfLegends = 101,
    HeroesOfTheStorm = 102,
    Smite = 103,
    Vainglory = 104,
    ArenaOfValor = 105,
    Paragon = 106,
    HeroesOfNewerth = -100,
};
class MobaGameEnumExtensions
{
public:
    [[nodiscard]] static String ToIntegralString(const MobaGame &value)
    {
        using namespace Extensions;
        return EnumExtensions::ToIntegralString(value);
    }
    [[nodiscard]] static int ToIntegral(const MobaGame &value)
    {
        using namespace Extensions;
        return EnumExtensions::ToIntegral<MobaGame>(value);
    }
    [[nodiscard]] static std::string ToString(const MobaGame &value, bool includeEnumName = false)
    {
        using namespace Extensions;
        static const std::map<MobaGame, String> values = {
            {MobaGame::Dota2, "Dota2"},
            {MobaGame::LeagueOfLegends, "LeagueOfLegends"},
            {MobaGame::HeroesOfTheStorm, "HeroesOfTheStorm"},
            {MobaGame::Smite, "Smite"},
            {MobaGame::Vainglory, "Vainglory"},
            {MobaGame::ArenaOfValor, "ArenaOfValor"},
            {MobaGame::Paragon, "Paragon"},
            {MobaGame::HeroesOfNewerth, "HeroesOfNewerth"},
        };
        return includeEnumName ? "MobaGame::"s + values.at(value) : values.at(value);
    }
    [[nodiscard]] static MobaGame Parse(const int &value)
    {
        using namespace Exceptions;
        static const std::map<int, MobaGame> values = {
            {static_cast<int>(MobaGame::Dota2), MobaGame::Dota2},
            {static_cast<int>(MobaGame::LeagueOfLegends), MobaGame::LeagueOfLegends},
            {static_cast<int>(MobaGame::HeroesOfTheStorm), MobaGame::HeroesOfTheStorm},
            {static_cast<int>(MobaGame::Smite), MobaGame::Smite},
            {static_cast<int>(MobaGame::Vainglory), MobaGame::Vainglory},
            {static_cast<int>(MobaGame::ArenaOfValor), MobaGame::ArenaOfValor},
            {static_cast<int>(MobaGame::Paragon), MobaGame::Paragon},
            {static_cast<int>(MobaGame::HeroesOfNewerth), MobaGame::HeroesOfNewerth},
        };
        try
        {
            return values.at(value);
        }
        catch (...)
        {
            throw ParseException("MobaGame::Parse"s);
        }
    }
    [[nodiscard]] static MobaGame Parse(const String &value)
    {
        using namespace Exceptions;
        using namespace Extensions;
        static const std::map<String, MobaGame> values = {
            {"Dota2", MobaGame::Dota2},
            {"LeagueOfLegends", MobaGame::LeagueOfLegends},
            {"HeroesOfTheStorm", MobaGame::HeroesOfTheStorm},
            {"Smite", MobaGame::Smite},
            {"Vainglory", MobaGame::Vainglory},
            {"ArenaOfValor", MobaGame::ArenaOfValor},
            {"Paragon", MobaGame::Paragon},
            {"HeroesOfNewerth", MobaGame::HeroesOfNewerth},
            {"MobaGame::Dota2"s, MobaGame::Dota2},
            {"MobaGame::LeagueOfLegends"s, MobaGame::LeagueOfLegends},
            {"MobaGame::HeroesOfTheStorm"s, MobaGame::HeroesOfTheStorm},
            {"MobaGame::Smite"s, MobaGame::Smite},
            {"MobaGame::Vainglory"s, MobaGame::Vainglory},
            {"MobaGame::ArenaOfValor"s, MobaGame::ArenaOfValor},
            {"MobaGame::Paragon"s, MobaGame::Paragon},
            {"MobaGame::HeroesOfNewerth"s, MobaGame::HeroesOfNewerth},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::Dota2)), MobaGame::Dota2},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::LeagueOfLegends)), MobaGame::LeagueOfLegends},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::HeroesOfTheStorm)), MobaGame::HeroesOfTheStorm},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::Smite)), MobaGame::Smite},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::Vainglory)), MobaGame::Vainglory},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::ArenaOfValor)), MobaGame::ArenaOfValor},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::Paragon)), MobaGame::Paragon},
            {IntegralExtensions::ToString(static_cast<int>(MobaGame::HeroesOfNewerth)), MobaGame::HeroesOfNewerth},
        };
        try
        {
            return values.at(value);
        }
        catch (...)
        {
            throw ParseException("MobaGame::Parse"s);
        }
    }
    [[nodiscard]] static std::vector<MobaGame> GetValues()
    {
        return {
            MobaGame::Dota2,
            MobaGame::LeagueOfLegends,
            MobaGame::HeroesOfTheStorm,
            MobaGame::Smite,
            MobaGame::Vainglory,
            MobaGame::ArenaOfValor,
            MobaGame::Paragon,
            MobaGame::HeroesOfNewerth,
        };
    }
};
std::ostream &operator<<(std::ostream &os, const MobaGame &value)
{
    os << MobaGameEnumExtensions::ToString(value);
    return os;
}

Using this

EZNUM_ENUM_UT(MobaGame,int32_t
        Dota2, EQ, 100,
        LeagueOfLegends, EQ, 101,
        HeroesOfTheStorm, EQ, 102,
        Smite, EQ, 103,
        Vainglory, EQ, 104,
        ArenaOfValor, EQ, 105,
        Paragon, _, _,
        HeroesOfNewerth, _, _)

` It generates

enum class MobaGame : int32_t
{
    Dota2 = 100,
    LeagueOfLegends = 101,
    HeroesOfTheStorm = 102,
    Smite = 103,
    Vainglory = 104,
    ArenaOfValor = 105,
    Paragon,
    HeroesOfNewerth,
};
class MobaGameEnumExtensions
{
public:
    [[nodiscard]] static String ToIntegralString(const MobaGame &value)
    {
        using namespace Extensions;
        return EnumExtensions::ToIntegralString(value);
    }
    [[nodiscard]] static int32_t ToIntegral(const MobaGame &value)
    {
        using namespace Extensions;
        return EnumExtensions::ToIntegral<MobaGame>(value);
    }
    [[nodiscard]] static std::string ToString(const MobaGame &value, bool includeEnumName = false)
    {
        using namespace Extensions;
        static const std::map<MobaGame, String> values = {
            {MobaGame::Dota2, "Dota2"},
            {MobaGame::LeagueOfLegends, "LeagueOfLegends"},
            {MobaGame::HeroesOfTheStorm, "HeroesOfTheStorm"},
            {MobaGame::Smite, "Smite"},
            {MobaGame::Vainglory, "Vainglory"},
            {MobaGame::ArenaOfValor, "ArenaOfValor"},
            {MobaGame::Paragon, "Paragon"},
            {MobaGame::HeroesOfNewerth, "HeroesOfNewerth"},
        };
        return includeEnumName ? "MobaGame::"s + values.at(value) : values.at(value);
    }
    [[nodiscard]] static MobaGame Parse(const int32_t &value)
    {
        using namespace Exceptions;
        static const std::map<int32_t, MobaGame> values = {
            {static_cast<int32_t>(MobaGame::Dota2), MobaGame::Dota2},
            {static_cast<int32_t>(MobaGame::LeagueOfLegends), MobaGame::LeagueOfLegends},
            {static_cast<int32_t>(MobaGame::HeroesOfTheStorm), MobaGame::HeroesOfTheStorm},
            {static_cast<int32_t>(MobaGame::Smite), MobaGame::Smite},
            {static_cast<int32_t>(MobaGame::Vainglory), MobaGame::Vainglory},
            {static_cast<int32_t>(MobaGame::ArenaOfValor), MobaGame::ArenaOfValor},
            {static_cast<int32_t>(MobaGame::Paragon), MobaGame::Paragon},
            {static_cast<int32_t>(MobaGame::HeroesOfNewerth), MobaGame::HeroesOfNewerth},
        };
        try
        {
            return values.at(value);
        }
        catch (...)
        {
            throw ParseException("MobaGame::Parse"s);
        }
    }
    [[nodiscard]] static MobaGame Parse(const String &value)
    {
        using namespace Exceptions;
        using namespace Extensions;
        static const std::map<String, MobaGame> values = {
            {"Dota2", MobaGame::Dota2},
            {"LeagueOfLegends", MobaGame::LeagueOfLegends},
            {"HeroesOfTheStorm", MobaGame::HeroesOfTheStorm},
            {"Smite", MobaGame::Smite},
            {"Vainglory", MobaGame::Vainglory},
            {"ArenaOfValor", MobaGame::ArenaOfValor},
            {"Paragon", MobaGame::Paragon},
            {"HeroesOfNewerth", MobaGame::HeroesOfNewerth},
            {"MobaGame::Dota2"s, MobaGame::Dota2},
            {"MobaGame::LeagueOfLegends"s, MobaGame::LeagueOfLegends},
            {"MobaGame::HeroesOfTheStorm"s, MobaGame::HeroesOfTheStorm},
            {"MobaGame::Smite"s, MobaGame::Smite},
            {"MobaGame::Vainglory"s, MobaGame::Vainglory},
            {"MobaGame::ArenaOfValor"s, MobaGame::ArenaOfValor},
            {"MobaGame::Paragon"s, MobaGame::Paragon},
            {"MobaGame::HeroesOfNewerth"s, MobaGame::HeroesOfNewerth},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::Dota2)), MobaGame::Dota2},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::LeagueOfLegends)), MobaGame::LeagueOfLegends},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::HeroesOfTheStorm)), MobaGame::HeroesOfTheStorm},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::Smite)), MobaGame::Smite},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::Vainglory)), MobaGame::Vainglory},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::ArenaOfValor)), MobaGame::ArenaOfValor},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::Paragon)), MobaGame::Paragon},
            {IntegralExtensions::ToString(static_cast<int32_t>(MobaGame::HeroesOfNewerth)), MobaGame::HeroesOfNewerth},
        };
        try
        {
            return values.at(value);
        }
        catch (...)
        {
            throw ParseException("MobaGame::Parse"s);
        }
    }
    [[nodiscard]] static std::vector<MobaGame> GetValues()
    {
        return {
            MobaGame::Dota2,
            MobaGame::LeagueOfLegends,
            MobaGame::HeroesOfTheStorm,
            MobaGame::Smite,
            MobaGame::Vainglory,
            MobaGame::ArenaOfValor,
            MobaGame::Paragon,
            MobaGame::HeroesOfNewerth,
        };
    }
};
std::ostream &operator<<(std::ostream &os, const MobaGame &value)
{
    os << MobaGameEnumExtensions::ToString(value);
    return os;
}

Full code

You can test/run/any this code from

It can't be pasted here. Too long code.

Here. The main program part

namespace Enums
{
    EZNUM_ENUM_UT(Variables, int,
        X, _, _,
        Y, EQ, 25,
        Z, EQ, 75)

        EZNUM_ENUM_UT(Fruit, int32_t,
            PEAR, EQ, -100,
            APPLE, _, _,
            BANANA, _, _,
            ORANGE, EQ, 100,
            MANGO, _, _,
            STRAWBERRY, EQ, 75,
            WATERMELON, EQ, 100)

        EZNUM_ENUM(Animal,
            Dog, _, _,
            Cat, _, _,
            Monkey, EQ, 50,
            Fish, _, _,
            Human, EQ, 100,
            Duck, _, _,
            __COUNT, _, _)

        EZNUM_ENUM_UT(MathVars32, int32_t,
            X, _, _,
            Y, _, _,
            Z, EQ, 75)

        EZNUM_ENUM_UT(MathVars64, int64_t,
            X, _, _,
            Y, _, _,
            Z, EQ, 75)

        EZNUM_ENUM(Vowels,
            A, EQ, 75,
            E, _, _,
            I, EQ, 1500,
            O, EQ, -5,
            U, _, _)

        EZNUM_ENUM(MobaGame,
            Dota2, EQ, 100,
            LeagueOfLegends, EQ, 101,
            HeroesOfTheStorm, EQ, 102,
            Smite, EQ, 103,
            Vainglory, EQ, 104,
            ArenaOfValor, EQ, 105,
            Paragon, EQ, 106,
            HeroesOfNewerth, EQ, -100)
}



#define PRINT_VALUES(Name) std::cout << "EnumName: "s + #Name << std::endl; \
std::cout << StringExtensions::PadRight(EMPTY_STRING , 21 + 128, '_') << std::endl; \
for (Name element : Name##EnumExtensions::GetValues()) \
{ \
    std::cout << StringExtensions::PadRight(Name##EnumExtensions::ToString(element), 16) << " | " << \
        StringExtensions::PadRight(Name##EnumExtensions::ToString(element, true),32) << " | " << \
        StringExtensions::PadRight(Name##EnumExtensions::ToIntegralString(element),8) << " | " << \
        StringExtensions::PadRight(IntegralExtensions::ToString(Name##EnumExtensions::ToIntegral(element)),8) << " | " << \
        StringExtensions::PadRight(Name##EnumExtensions::ToString(Name##EnumExtensions::Parse(Name##EnumExtensions::ToString(element))),16) << " | " << \
        StringExtensions::PadRight(Name##EnumExtensions::ToString(Name##EnumExtensions::Parse(Name##EnumExtensions::ToString(element, true))),16) << " | " << \
        StringExtensions::PadRight(Name##EnumExtensions::ToString(Name##EnumExtensions::Parse(Name##EnumExtensions::ToIntegralString(element))),16) << " | " << \
        StringExtensions::PadRight(Name##EnumExtensions::ToString(Name##EnumExtensions::Parse(Name##EnumExtensions::ToIntegral(element))),16) << std::endl; \
} \
std::cout<< std::endl;




int main() {

    using namespace Enums;
    using namespace Extensions;
    PRINT_VALUES(Variables)
    PRINT_VALUES(Fruit)
    PRINT_VALUES(Animal)
    PRINT_VALUES(MathVars32)
    PRINT_VALUES(MathVars64)
    PRINT_VALUES(Vowels)
    PRINT_VALUES(MobaGame)
/*  std::cout << "EnumName: "s + "MobaGame" << std::endl;
    std::cout << StringExtensions::PadRight(EMPTY_STRING, 21 + 128, '_') << std::endl;
    for (MobaGame element : MobaGameEnumExtensions::GetValues())
    {
        std::cout << StringExtensions::PadRight(MobaGameEnumExtensions::ToString(element), 16) << " | " << StringExtensions::PadRight(MobaGameEnumExtensions::ToString(element, true), 32) << " | " << StringExtensions::PadRight(MobaGameEnumExtensions::ToIntegralString(element), 8) << " | " << StringExtensions::PadRight(IntegralExtensions::ToString(MobaGameEnumExtensions::ToIntegral(element)), 8) << " | " << StringExtensions::PadRight(MobaGameEnumExtensions::ToString(MobaGameEnumExtensions::Parse(MobaGameEnumExtensions::ToString(element))), 16) << " | " << StringExtensions::PadRight(MobaGameEnumExtensions::ToString(MobaGameEnumExtensions::Parse(MobaGameEnumExtensions::ToString(element, true))), 16) << " | " << StringExtensions::PadRight(MobaGameEnumExtensions::ToString(MobaGameEnumExtensions::Parse(MobaGameEnumExtensions::ToIntegralString(element))), 16) << " | " << StringExtensions::PadRight(MobaGameEnumExtensions::ToString(MobaGameEnumExtensions::Parse(MobaGameEnumExtensions::ToIntegral(element))), 16) << std::endl;
    }
    std::cout << std::endl;*/
    std::cin.get();
    return 0;
}

Output

VisualC++

vc++

Upvotes: 2

Jarod42
Jarod42

Reputation: 218138

Alternatively, you might check that the zero value is not present in the array:

static constexpr const char * color_names[max_item] = {
   "red",
   "green",
   "blue"
};

static_assert(std::ranges::find(color_names, nullptr) == std::end(color_names));

Demo

Upvotes: 2

Ted Lyngmo
Ted Lyngmo

Reputation: 117832

The easiest would probably be to remove max_item from the array size and let it be automatically sized and then static_assert on the size:

static const char* color_names[] = { // `max_item` removed here
   "red",
   "green",
   "blue"
};

// and a static_assert added after the definition of the array:
static_assert(std::size(color_names) == max_item);

Upvotes: 6

Related Questions