10

I'm trying to implement compile-time polymorphism using CRTP, and want to force the derived class to implement the function.

The current implementation is like this.

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->f();
    }
};

struct derived : base<derived>
{
    void f() {
    ...
    }
};

In this implementation, call to the function falls into an infinite loop if the derived class didn't implement f().

How do I force the derived class to implement the function like pure virtual function? I tried to use 'static_assert' like static_assert(&base::f != &Derived::f, "...") but it generates an error message saying that two member function pointers pointing to the different classes' member functions are not comparable.

Inbae Jeong
  • 4,053
  • 25
  • 38
  • Take a look at `ctype::scan_is` and `ctype::do_scan_is`. – user541686 Feb 09 '15 at 06:02
  • That is odd, I don't get any such errors if using the static_assert trick. Has something related to this changed in the standard since 2015? See: https://godbolt.org/z/cY7an9cz5 – Fabio A. Nov 28 '22 at 07:11

3 Answers3

9

You can give the thing you override and the hook different names, like this:

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->fimpl();
    }
    void fimpl() = delete;
};

struct derived : base<derived> {
    void fimpl() { printf("hello world\n"); }
};

Here, fimpl = delete in the base so that it cannot be called accidentally unless fimpl is overridden in the derived class.

You can also stick an intermediate hiding layer into your CRTP to "temporarily" mark f as delete:

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->f();
    }
};

template <class Derived>
struct intermediate : base<Derived> {
    void f() = delete;
};

struct derived : intermediate<derived> {
    void f() { printf("hello world\n"); }
};
tmyklebu
  • 13,915
  • 3
  • 28
  • 57
  • The second solution putting an intermediate class is wonderful. – Inbae Jeong Feb 09 '15 at 06:23
  • I'd do it the first way. The second way risks an F-up if you forget to inherit from the intermediate class. – tmyklebu Feb 09 '15 at 06:26
  • I guess that's just a naming problem. What about putting the real base into `detail` namespace and name the intermediate class `base`? – Inbae Jeong Feb 09 '15 at 06:41
  • You can do stuff like that if you like. Whenever you want to talk about the real base, though, you have to call it `detail::base`, and that's everywhere except where you inherit. I generally prefer not to complicate code more than necessary, but I'm also not in the business of maximising billable hours. – tmyklebu Feb 09 '15 at 07:08
  • I get your point, but you also have to remember the function names for the interface and implementation anyway if they differ. – Inbae Jeong Feb 09 '15 at 10:02
3
template<typename Derived>
class Base
{
  private:
    static void verify(void (Derived::*)()) {}

  public:
    void f()
    {
        verify(&Derived::f);
        static_cast<Derived*>(this)->f();
    }
};

If the derived class does not implement f on its own, the type of &Derived::f would be void (Base::*)(), which breaks compilation.

Since C++11 we can also make this function generic with variadic template.

template<typename Derived>
class Base
{
  private:
    template<typename T, typename...Args>
    static void verify(T (Derived::*)(Args...)) {}
};
jdh8
  • 3,030
  • 1
  • 21
  • 18
0

Through this is a question asked years ago but I've encountered this recently, so I'll just post it here hope it might help some people.

Using auto as return type might be another solution. Consider the following code:

template<typename Derived>
class Base
{
  public:
    auto f()
    {
        static_cast<Derived*>(this)->f();
    }
};

If the derived class doesn't provide a valid overload, then this function becomes recursive, and since auto requires the final return type, it can never be deduced, thus would be guaranteed to throw an compilation error. For example on MSVC it's something like:

a function that returns 'auto' cannot be used before it is defined

This forces derived class to provide implementation, just like pure virtual function.

The good thing is no extra code is needed, and if the derived class also use auto as return type then this chain can go as long as required. In some cases it can be convenient and flexible, as in Base and LevelTwo in following code, which can return different types when calling the same interface f. However this chain totally disables direct inheritance of implementation from base class, as in LevelThree:

template<typename Derived = void>
class Base
{
  public:
    Base() = default;
    ~Base() = default;

    // interface
    auto f()
    {
        return fImpl();
    }
  protected:
    // implementation chain
    auto fImpl()
    {
        if constexpr (std::is_same_v<Derived, void>)
        {
            return int(1);
        }
        else
        {
            static_cast<Derived*>(this)->fImpl();
        }
    }
};

template<typename Derived = void>
class LevelTwo : public Base<LevelTwo>
{
  public:
    LevelTwo() = default;
    ~LevelTwo() = default;

    // inherit interface
    using Base<LevelTwo>::f;
  protected:
    // provide overload
    auto fImpl()
    {
        if constexpr (std::is_same_v<Derived, void>)
        {
            return float(2);
        }
        else
        {
            static_cast<Derived*>(this)->fImpl();
        }
    }

    friend Base;
};

template<typename Derived = void>
class LevelThree : public LevelTwo<LevelThree>
{
  public:
    LevelThree() = default;
    ~LevelThree() = default;

    using LevelTwo<LevelThree>::f;

  protected:
    // doesn't provide new implementation, compilation error here
    using LevelTwo<LevelThree>::fImpl;

    friend LevelTwo;
};

In my case, the derived class I work on also derive from another class which provides extra information needed to determine whether to stop at current class or go for derived class. But in other cases, either break the chain using actual types instead of 'auto', or use some other tricks. But in situations like that maybe virtual function is the best chose.

X. Sun
  • 315
  • 3
  • 15