I am designing an internal API that deals with images. As part of this, I hit this use case where:
- I specify an image format as a single value.
- An image format implies a collection of settings (values to pass to OpenGL, fragment shader code, texel bit size, …).
In essence, a trait would be perfect… except the image format is not known at compile time.
I am looking for a good way to express this in C++ (C++17 allowable).
So far, here is an attempt that works but feels very awkward:
// pixel_format_def is a structure with static constexpr members
using R8 = detail::pixel_format_def<GL_R8, GL_RED, GL_UNSIGNED_BYTE>;
using RG8 = detail::pixel_format_def<GL_RG8, GL_RG, GL_UNSIGNED_BYTE>;
// ... many others
using PixelFormat = std::variant<R8, RG8>;
The actual pixel_format_def
has more fields. It is then used like this:
void someFunc(PixelFormat format /*, other args */)
{
std::visit([&](auto tag) {
using Format = decltype(tag);
someFunc(Format::format, Format::type/* other args */);
}, format);
}
Basically this does tag dispatching, with std::variant
holding a tag.
It fulfills my requirements:
If the format is known at compile time, the compiler inlines the call and removes the
std::visit
. For instance:someFunc(RG8());
will be inlined (at least by clang and gcc) directly to
someFunc(GL_RG, GL_UNSIGNED_BYTE);
Yet it works if the format is only known at runtime. In which case std::visit will pick the correct values Without passing around a big structure.
It feels really awkward though, almost a mis-use of std::variant
. And I am not sure it would pass code review. Any better idiom?
map
or similar object? Is there some memory overhead issue that you have?someFunc(RG8())
, the compiler inlines it assomeFunc(GL_RG, GL_UNSIGNED_BYTE)
. Neat and efficient, no need to go looking for the values in some runtime structure. — I just added that precision to the question btw.someFunc
to be "inlined", it'd have to be a template, right? Which means you have to instantiatesomeFunc
for every image format type. Which means you will have a lot of these lying around, so if you're calling a bunch of differentsomeFunc<type>
instantiations, you'll miss a lot on the instruction cache.