Search code examples
c++oopinheritancepolymorphism

Define a common interface without dynamic polymorphism


In applications such as ray tracing, an entity can be one of several types, which all share a common interface. For example, a Material can either be a DiffuseMaterial or a ReflectiveMaterial, etc and they all support a method Color getColor(args); In most applications, this issue is typically resolved using dynamic polymorphism:

class Material {
public:
  virtual Color getColor() = 0;
  // ...
}
class DiffuseMaterial: public Material {
public:
  Color getColor() {
    // ...
  }
}
class ReflectiveMaterial: public Material {
public:
  Color getColor() {
    // ...
  }
}

Then, a function can use Material* to represent a material, which is either of these types. This virtual class hierarchy not only allows the user to define a common behavior, it forces the user to define the virtual function, otherwise the programmer cannot create instances of the derived classes, because they are abstract. However, dynamic polymorphism causes a performance overhead due to the dynamic dispatch and lookup. This is a problem in high-performance programs.

More modern languages provide alternatives to dynamic polymorphism. Rust for example has traits (interfaces, which can be resolved at compile-time) and enums (the entities of which can be structs), which avoid dynamic polymorphism. For example,

enum Material {
  DiffuseMaterial{member1, member2}, // a struct
  ReflectiveMaterial {member1, member2, member3, } // a struct
}


How to achieve the same effect of dynamic polymorphism using compile-time polymorphism features in C++20? Namely, define a type, which can be one of several types. Forcing the programmer to define a custom implementation of the common behavior for each subtype would be nice, but isn't a must.


Solution

  • You could create a concept that checks all the requirements that you have on a Material:

    template <class T>
    concept IMaterial = requires(T m) {
        // list the requirements here
        { m.getColor() } -> std::same_as<Color>;
    };
    

    The actual classes implementing the requirements can then be completely unrelated:

    class DiffuseMaterial {
    public:
        Color getColor() { return Color{}; }
    };
    class ReflectiveMaterial {
    public:
        Color getColor() { return Color{}; }
    };
    

    ... and you can use the concept to only accept types that fulfills the requirements.

    Example:

    template<IMaterial M>   // M must fulfill the requirements
    class Foo : public M {  // or composition if that makes more sense
        void func() {
            Color c = this->getColor();
        }
    };
    

    Demo