Skip to content

Instantly share code, notes, and snippets.

@neoblizz
Last active December 7, 2021 19:33
Show Gist options
  • Save neoblizz/254fc21a137346591f0b99e77b7469d2 to your computer and use it in GitHub Desktop.
Save neoblizz/254fc21a137346591f0b99e77b7469d2 to your computer and use it in GitHub Desktop.
Capturing conditional inheritance in CPP (with pixel shaders as a toy example)

The Problem

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.

Obvious approach

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:

Obvious compile-time approach

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

Modern-C++ approach

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.

Better approach; using C++ classes insead

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;

The if, else if, else in inheritance

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.

Even better approach (imo): variadic inheritance

Let's revise our goals:

  1. 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).
  2. To avoid using if-else statements or similar conditionals.
  3. To support compile-time nature of these types.
  4. 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

@neoblizz
Copy link
Author

neoblizz commented Dec 7, 2021

My pixel/texture example may be too much of a toy example. I don't know graphics well... 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment