We have a top-level object that the user wants to interact with, such as a pixel on the screen. But given the contents within that pixel, it may choose to color/shade it differently. If that pixel is representing a cloth, it may have a texture and color of a cloth, if it is representing metal, it may be shiny and metal-like... you get the point. To represent this object in c++, we have number of options. The most obvious one is to have a function that colors (or applies some sort of texture) to the pixel, and has the different specializations for the materials/colors within that function.
void apply_texture(pixel_t* p, texture_t t) {
if(t == texture_t::cloth) {
// apply cloth
} else if (t == texture_t::skin) {
// apply skin
} else {
// apply metal
}
}
All of the above conditionals are evaluated at runtime, texture is passed in at runtime. Now, that maybe not the best option since you may have things at compile time. So, you can simply do the following:
template<texture_t T>
void apply_texture(pixel_t* p) {
#if T == texture_t::cloth
// apply cloth
#elif T == texture_t::skin
// apply skin
#else
// apply metal
#endif
}
Gross, but works. You can do this better by using modern-cpp's constexpr
:
constexpr functions can only depend on functionality which is a constant expression. Being a constexpr function does not mean that the function is executed at compile time. It says, that the function has the potential to run at compile time. A constexpr function can also run a runtime. - moderncpp.com
template<texture_t T>
void apply_texture(pixel_t* p) {
if constexpr(T == texture_t::cloth) {
// apply cloth
} else if constexpr(T == texture_t::skin) {
// apply skin
} else {
// apply metal
}
}
I think all the if, else if, else statements just make the code look ugly. And in professional systems, the statements may get out of hand really fast. Imagine dealing with a 100+ of those statements, even with the modern-cpp constexpr
, which takes care of runtime/compile-time options it is not a nice thing to look at.
class pixel_t {
};
We can have a higher-level class, that the user cares about, that specializes using other classes, like the following sub-classes:
class cloth_t {
// apply cloth texture
};
class skin_t {
// apply skin texture
};
class metal_t {
// apply metal texture
};
Now, the above classes could be implemented using virtual functions and can replace the virtual functions in the pixel_t
class using the specialized texture classes. The problem however, is how do you choose which class to inherit? and how is that implemented? Let's take a look at some tools that cpp supports to do that;
We can again default to the conditionals for inheritance, and use if-else statements and an enum to inherit the right texture class. For example;
typename <bool is_cloth> // we can use an enum here for all types insead.
class pixel_t : std::conditional_t<is_cloth, cloth_t, empty_t> {
};
The above code suggests that if is_cloth
boolean is true, then the pixel_t
needs to inherit cloth_t
, otherwise, it inherits an empty_t
type (which can be thought of void
type). You can repeat the conditionals to support all the texture types. This can also be implemented using std::enable_if
. Now, the nice thing about this is that your class will only have the code of the necessary texture, and won't contain any of the other textures if they are not being applied, unlike the function-based approach shown earlier. But, the problem of using a bunch of if-else statements is still there, just hidden behind a std::conditional
now. Again, the same will be true for std::enable_if
and you'll have to update these conditionals for all the types/structs/textures that you'd like to support.
IMPORTANT: So... anytime you need to add support for new textures, you'd have to update these conditionals and all of their occurences where the new texture needs to be supported.
Let's revise our goals:
- To not have any texture code in our object that is not needed/necessary. Only have the texture code that we actually use. (fixed by using classes/inheritance).
- To avoid using if-else statements or similar conditionals.
- To support compile-time nature of these types.
- Doesn't need to be updated (because we avoid conditionals) every time a new texture type is added.
template<class... texture_view_t>
class pixel_t : public texture_view_t... {
};
The above code with template<class... texture_view_t>
uses C++ variadic arguments to inherit whatever type is needed, without having to use conditionals and is done at compile-time (you can create instances of all the different paths to support a runtime selection as well). Ok, how do we use this with the textures?
using texture_t = cloth_t; // I want the pixel to be of cloth texture.
pixel_t<texture_t> p; // Now, p is my pixel that inherits whatever type is in texture_t.
And! I can compose together multiple types and inherit them all to my pixel_t
:
using first_texture_t = glass_t;
using second_texture_t = shatter_t;
pixel_t<first_texture_t, second_texture_t> p;
The above pixel will inherit both glass_t
and shatter_t
classes. If that is helpful...
This is the current solution I use for representing graphs in graph analytics that have multiple underlying sparse views (storage-types): https://github.com/gunrock/essentials/blob/master/include/gunrock/graph/graph.hxx#L28-L56
My pixel/texture example may be too much of a toy example. I don't know graphics well... 😅