Skip to content

JessyDL/strtype

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

strtype

C++20 support for stringifying enums and typenames at compile time (with constraints) for The Big Three compilers (GCC, CLang, and MSVC).

How it works

(Ab)using the compiler injected macro/extension __FUNCSIG__ or __PRETTY_FUNCTION__, at compile time parse the resulting string to extract the enum value or typename. In the case of enum values we will be rejecting entries that do not point to named values.

I don't take responsibity for when this breaks, compiler provided macros and extensions can always be changed, but as of writing (and looking at historical behaviour of these 2 values) they appear to be pretty stable. As of now none of the code goes explicitly against the standard, nor relies on UB (aside from the compiler specific macro/extension usage) and so I foresee this to keep working for some time.

Limitations

These limitations really only apply to enums, typenames do not have any limitations aside from recovering alias names. Due to how the compilers interact with alias typenames they are substituted before we can recover that information.

The enum types are currently restricted to anything that satisfies std::is_integral, and std::is_scoped_enum. The integral limitation isn't really needed, it just lowers the potential oversight of unforeseen issues. Feel free to implement the std::is_arithmetic constraint, add tests, and make a PR if you're up for it!

Search depth: As we need to iterate over all potential values that are present in the enum, and as iterating over the entire 2^n bits of the underlying type is too heavy; we limit the search depth by default to 1024, and offer 2 different iteration techniques (strtype::sequential_searcher, and strtype::bitflag_searcher). Both of these are tweakable, see the following section for info on how to do so.

CLang has a further limitation of how many times a fold expression can be expanded (256). We have a small workaround for this implemented in the strtype::sequential_searcher (one layer of indirection), but it means when the range exceeds 255 * 255 on CLang, the compilation will fail unless this limit is increased.

Duplicate named values (i.e. multiple enum names on the same value) will only fetch the first name it sees. So when the following enum is defined:

enum class foo {
    bar,
    tan = bar,
    cos,
    sin,
};

The output will be an array of { "bar", "cos", "sin" }, notice the missing "tan" (see this on godbolt). This is because during compilation foo::tan is considered an alias of foo::bar, and will be substituted. There is no way of recovering this information.

Usage & customization points

The two entry functions into stringifying your enums/types are strtype::stringify<TYPE/VALUE>() and strtype::stringify_map<YOUR_ENUM_TYPE>(). The first function will return you an std::array<std::string_view> if given an enum type, otherwise when given an enum value it will return you a std::string_view representation of the enum value. In the case of the array return the values are sorted based on the underlying enum values. The stringify_map function will return you a compile and runtime searchable associative container where you can search for the enum value based on its string representation and vice-versa.

Your enums should either come with a _BEGIN/_END sentinel values in the enum declaration, or you should specialize the strtype::enum_information customization point (see example section). Note that both the specialized END and the embedded _END act as inclusive limits to the range. This means unlike normal ranges, which are exclusive ranges, the endpoint is used as the last value. This is the mathematical difference of [0,10] (range of 0 to 10, inclusive) and [0,10) (a range of 0 to 9, excluding 10). This was done for convenience so that users don't need to define END as END = some_value + 1. This is only the case when within the enum declaration scope, or when END is set as an instance of the enum type object; if it's set as its underlying type then it behaves like an exclusive range limitter again.

By default the search iterations is limited to 1024, this means if the difference between the first and last enum value is larger than that, you'll either have to specialize strtype::enum_information for your type, or globally override the default value by defining strtype_MAX_SEARCH_SIZE with a higher value.

Lastly the search pattern. There are 2 provided search patterns strtype::sequential_searcher and strtype::bitflag_searcher. Both will search from _BEGIN to _END, but have a different approach.

  • sequential_searcher: iterates over the range by adding the lowest integral increment for the underlying type from BEGIN to END.
  • bitflag_searcher: iterates over the range by jumping per bit value instead (so an 8bit type will have 8 iterations, one for every bit + the 0 value). Combinatorial values are not searched for. For example if there is a value at 0x3, which would be both first and second bit set, it would be skipped.

You can provide your own searcher, as long as it satisfies the following API:

struct custom_searcher {
  template<typename T, auto Begin, auto End>
  consteval auto max_size() const noexcept -> size_t { /* return the theorethical max value for your enum type */ }

  template<typename T, auto Begin, auto End>
  consteval auto operator() const noexcept -> std::array<std::string_view, /* size must be calculated internally */>
};

See strtype::sequential_searcher, or strtype::bitflag_searcher for example implementations. Note that at least the sequential_searcher has some compiler specific performance optimizations and workarounds which do complicate the code a bit.

Examples

compile time stringify an enum (godbolt)

Following example showcases an enum being stringified, and accessible as a contiguous array of std::string_view's.

enum class foo {
  bar, tan, cos, sin,
};

namespace strtype {
  template<>
  struct enum_information<foo> {
    using SEARCHER = strtype::sequential_searcher;
    static constexpr auto BEGIN      { foo::bar };
    static constexpr auto END        { foo::sin };
    // static constexpr size_t MAX_SEARCH_SIZE { 2048 }; // optionally override the search size, this isn't needed unless you hit the limit.
  };
}

int main() {
  constexpr auto values = strtype::stringify<foo>();
  static_assert(values[0] == "bar");
  return 0;
}

compile time stringify a single enum value (godbolt)

Here we stringify only a single value of the enum, note the abscence of the customization point as it's unneeded. The return type of this function is a unique type that holds the string as a NTTP.

enum class foo {
  bar, tan, cos, sin,
};

int main() {
  // note: the return type of foobar_str is a type containing the string as NTTP argument.
  constexpr auto foobar_str = strtype::stringify<foo::bar>();
  static_assert(foobar_str == std::string_view { "bar" });
  return 0;
}

runtime stringify enum values (godbolt)

The following example stores the values into a compile, or runtime searchable map. We then use (randomly) get the string based representation using the enum value. The reverse is also possible (string search to enum value).

#include <random>
#include <cstdio>
enum class foo {
  bar, tan, cos, sin, _BEGIN = bar, _END = sin,
};

constexpr auto foo_map = strtype::stringify_map<foo>();

int main() {
  std::random_device rd;
  std::mt19937 mt(rd());
  std::array<foo, 4> foo_values{foo::bar, foo::tan, foo::cos, foo::sin };
  std::uniform_int_distribution<size_t> rnd(0, foo_values.size() - 1);
  for(auto i = 0; i < 32; ++i)
  {
    std::printf("%s\n", foo_map[foo_values[rnd(mt)]].data());
  }
  return 0;
}

Licence

See the LICENSE file provided.

About

C++20 support for stringifying enums and typenames at compile time (with constraints)

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published