3

I'm trying to implement an inheritable visitor method for a class using CRTP instead of a virtual function. First we have some code like this:

#include <iostream>
#include <memory>
struct A {
    virtual ~A() = default;
};
struct B : A {};
struct C : A {};

template <typename Derived>
struct Base {
    void dispatch(A *a) {
        if (dynamic_cast<C *>(a)) {
            return static_cast<Derived*>(this)->func(dynamic_cast<C *>(a));
        }
        if (dynamic_cast<B *>(a)) {
            return static_cast<Derived*>(this)->func(dynamic_cast<B *>(a));
        }
        return static_cast<Derived*>(this)->func(a);
    }
    void func(A*) { std::cout << "Base::func(A&)\n"; }
    void func(B*) { std::cout << "Base::func(B&)\n"; }
    void func(C*) { std::cout << "Base::func(C&)\n"; }
private:
    Base() = default;
    friend Derived;
};

struct D1 : Base<D1> {
    void func(B*) { 
        std::cout << "D1::func(B&)\n"; 
    }
    void func(C*) { std::cout << "D1::func(C&)\n"; }
    using Base::func;
};

struct D2 : Base<D2>, D1 {
    void func(C*) { std::cout << "D2::func(C&)\n"; }
    using Base<D2>::dispatch;
    using D1::func;
};

int main() {
    D2 d2;
    std::unique_ptr<A> a = std::make_unique<A>();
    std::unique_ptr<A> b = std::make_unique<B>();
    std::unique_ptr<A> c = std::make_unique<C>();
    
    d2.dispatch(a.get());
    d2.dispatch(b.get());
    d2.dispatch(c.get());
}

and then it output this:

~/dev1/develop$ ./test2
Base::func(A&)
D1::func(B&)
D2::func(C&)

It looks good; the derived visitor calls its base class's function just like a virtual function. In this way, when our several visitors have the same access patterns for most types, and only a few types need to be distinguished, we can implement only the functions that we care about.

But things goes wrong when we try to recursively call visit(), which is common when the visited object has some children nodes. We tried to modify struct D1:

struct D1 : Base<D1> {
    void func(B*) { 
        std::cout << "D1::func(B&)\n"; 

        // c is b's children node.
        std::unique_ptr<A> c = std::make_unique<C>();
        // usually two ways of visit children, differs if we can know c's real type here.
        dispatch(c.get());
        func(static_cast<C*>(c.get()));
    }
    void func(C*) { std::cout << "D1::func(C&)\n"; }
    using Base::func;
};

the output becomes:

~/dev1/develop$ ./test2
Base::func(A&)
D1::func(B&)
D1::func(C&)
D1::func(C&)
D2::func(C&)

So we found it calls the wrong function which is not expected. It does not behave similarly to a virtual function now.

The reason for this error and the solution are easy to understand; simply using virtual functions will solve the problem. I also know that virtual functions aren't the main performance issue here. However, I'd still like to know if there are techniques using CRTP that can achieve the same result?

The same implementation using inherited virtual functions:

struct Base {
    void dispatch(A *a) {
        if (dynamic_cast<C *>(a)) {
            return func(dynamic_cast<C *>(a));
        } else if (dynamic_cast<B *>(a)) {
            return func(dynamic_cast<B *>(a));
        } else {
            return func(a);
        }
    }

    virtual void func(A*) { std::cout << "Base::func(A&)\n"; }
    virtual void func(B*) { std::cout << "Base::func(B&)\n"; }
    virtual void func(C*) { std::cout << "Base::func(C&)\n"; }
};

struct D1 : Base {
    void func(B*) override {
        std::cout << "D1::func(B&)\n"; 

        // assume c is b's children node.
        std::unique_ptr<A> c = std::make_unique<C>();
        // usually two ways of visit children, differs if we can know c's real type here.
        dispatch(c.get());
        func(static_cast<C*>(c.get()));
    }

    void func(C*) override {
        std::cout << "D1::func(C&)\n";
    }
};

struct D2 : D1 {
    void func(C*) override {
        std::cout << "D2::func(C&)\n";
    }
};
3
  • 5
    ...which is not our expected... What is your expected? Commented Dec 2 at 18:07
  • Perhaps refer to the design of clang::RecursiveASTVisitor Commented Dec 3 at 4:37
  • Looks like you've got the worst of both worlds. dynamic_cast has worse performance, worse readability and worse maintainability than the plain old virtual call, and it doesn't work like you expect it to. Commented Dec 3 at 7:28

1 Answer 1

7

With deducing this/Explicit_object_parameter (C++23), you might get rid of CRTP and know the (initial) static type of this, and do:

struct Base {
    void dispatch(this auto& self, A* a) {
        if (auto* p = dynamic_cast<C*>(a)) {
            return self.func(p);
        }
        if (auto* p = dynamic_cast<B*>(a)) {
            return self.func(p);
        }
        return self.func(a);
    }
    void func(this auto&, A*) { std::cout << "Base::func(A&)\n"; }
    void func(this auto&, B*) { std::cout << "Base::func(B&)\n"; }
    void func(this auto&, C*) { std::cout << "Base::func(C&)\n"; }
};

struct D1 : Base {
    void func(this auto& self, B*) { 
        std::cout << "D1::func(B&)\n"; 

        // c is b's children node.
        std::unique_ptr<A> c = std::make_unique<C>();
        // usually two ways of visit children, differs if we can know c's real type here.
        self.dispatch(c.get());
        self.func(static_cast<C*>(c.get()));
    }
    void func(this auto&, C*) { std::cout << "D1::func(C&)\n"; }
    using Base::func;
};

struct D2 : D1 {
    void func(this auto&, C*) { std::cout << "D2::func(C&)\n"; }
    using D1::func;
};

Demo

Sign up to request clarification or add additional context in comments.

1 Comment

I think this answer is sufficient as a solution. Although my project is still using C++17, I found a way to write it using CRTP by reading "Deducing This" 's draft.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.