Calling potential variadic base class protected member functions

104 views Asked by At

For context: I'm constructing a CRTP Mixin for my derived class.

Now I want to call all protected base class member functions if they exist. I want to have the base class member functions not exposed to the user.

For this I have the following code:

struct A {
   protected:
    void fooImpl() { std::cout << "A::fooImpl"; }
};

struct C {};

template <typename... T>
struct B : T... {
    void foo() {
        auto visitor = [](auto& s) {
            if constexpr (requires { s.fooImpl(); }) 
                s.fooImpl();
        };
        (visitor(static_cast<T&>(*this)), ...);
    }
};

int main() {
    B<A, C> b;
    b.foo();
    return 0;
}

Godbolt This code does not call the fooImpl from C (as expected) and also not the one from A.

I assume this is due to the static_cast, which removes the protected visibility of fooImpl. Is this expected? I assume so when following the discussion from Access to protected constructor of base class, which also applies to my case, doesn't it?

I have two solutions, which I think are not elegant.

  1. Going the CRTP path and forward the derived class to the base to make derived a friend.
template <typename S>
struct A {
    friend S;

   private:
    void fooImpl() { std::cout << "A::fooImpl"; }
};

template <typename S>
struct C {};

template <template <typename> class... T>
struct B : T<B<T...>>... {
    void foo() {
        auto visitor = [](auto& s) {
            if constexpr (requires { s.fooImpl(); }) 
                s.fooImpl();
        };
        (visitor(static_cast<T<B<T...>>&>(*this)), ...);
    }
};

Godbolt This works but I wonder if there is a better way. I also have to refactor all the template signature of my class, which is not so nice.

  1. Hiding the base class foos by calling convention and rename fooImpl to foo
struct A {
   public:
    void foo() { std::cout << "A::foo"; }
};

struct C {};

template <typename... T>
struct B : T... {
    void foo() {
        auto visitor = [](auto& s) {
            if constexpr (requires { s.foo(); }) 
                s.foo();
        };
        (visitor(static_cast<T&>(*this)), ...);
    }
};

Godbolt

This also works but I also think maybe I have overengineered this. Anyway the base class member function are now also hidden to the user. Is there a nicer way to achieve this? Thanks!

EDIT: Refactor of the last question. Is there are more modern way to achieve this with less boilder-plate?

3

There are 3 answers

4
joergbrech On BEST ANSWER

Instead of checking if A implements a protected member function in your concept, you could check if this->A::fooImpl() is a valid statement for B.

To do this, capture this in your visitor lambda:

template <typename... T>
struct B : T... {
    void foo() {
        auto visitor = [this] <typename U>(U&) {
            if constexpr (requires { this->U::fooImpl(); }) this->U::fooImpl();
        };
        (visitor(static_cast<T&>(*this)), ...); // note: argument just passed for template argument deduction
    }
};

https://godbolt.org/z/s5Yohd9sM

Note that if you don't add U& as an argument to your lambda, the fold expression becomes (visitor.template operator()<T>(), ...); which is a bit awkward IMHO. Having a private template <typename U> void call_base_foo_impl() in B instead of the lambda might improve readability as well.

2
bbalazs On

Not sure if you find it nicer, but here you go:

#include <iostream>

struct A {
protected:
    void foo()
    {
        std::cout << "A::foo" << std::endl;
    }
};

struct C {};

template <typename... T>
struct B : private T...
{
    void foo()
    {
        // [[maybe_unused]] auto list = std::initializer_list<bool>{ (callFoo<T>(), true)...}; // pre-17                                                                                                           
        (callFoo<T>(), ...); // fold-expression
    }

private:
    
    template <typename U>
    void callFoo()
    {
        if constexpr (requires {U::foo(); })
        {
            U::foo();
        }
    }

};

int main()
{
    {
        B<A, C> b;
        b.foo();
    }
    {
        B<> b;
        b.foo();
    }
    return 0;
}
1
Marek R On

Problem is you need scope operator instead casting this to respective type. When using cast pointer you need public access. I've got nice C++17 solution (C++17 since I used fold expression, it is should be easy to tweak it to C++11):

template <typename... T>
struct B : T... {
    void foo()
    {
        (invokeFooImpl<T>(1), ...);
    }

private:
    template <typename Base>
    auto invokeFooImpl(int) -> decltype(Base::fooImpl())
    {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        Base::fooImpl();
    }

    template <typename Base>
    auto invokeFooImpl(char)
    {
        std::cout << __PRETTY_FUNCTION__ << '\n';
    }
};

https://godbolt.org/z/avExz4M9s

Or C++20 version without lamda:

template <typename... T>
struct B : T... {
    void foo()
    {
        (invokeFooImpl<T>(), ...);
    }

private:
    template <typename Base>
    auto invokeFooImpl()
    {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        if constexpr (requires { this->Base::fooImpl(); }) Base::fooImpl();
    }
};

https://godbolt.org/z/hvEb83zro