Home > Enterprise >  Templated member function and inheritence
Templated member function and inheritence

Time:01-13

I have a template member function declared in a class that call the correct member function depending on type, and want to add some functionality to it in a daughter class, by adding a member function, like in the main.cpp example below :

#include <iostream>

class A
{
public:
    template <typename T>
    void handleSocketData(const T& t)
    {
        handleData(t);
    }

    void handleData(int data)
    {
        std::cout << data << std::endl;
    }

};

class B: public A
{
public :
    void handleData(std::string data) const
    {
        std::cout << data << std::endl;
    }
};

int main(int argc, char *argv[])
{
    A a;
    B b;
    a.handleSocketData<int>(30);
    b.handleSocketData<std::string>("Hi");
    return 0;
}

My problem is that b.handleSocketData<QString>("Hi"); actually does generate a new template instance in A class as shown in the output of command /usr/bin/clang -DQT_CORE_LIB -isystem /usr/include/qt6/QtCore -isystem /usr/include/qt6 -isystem /usr/lib64/qt6/mkspecs/linux-g -g -std=gnu 17 -Xclang -ast-print -fsyntax-only main.cpp:

class A {
public:
    template <typename T> void handleSocketData(const T &t) {
        this->handleData(t);
    }
    template<> void handleSocketData<int>(const int &t) {
        this->handleData(t);
    }
    template<> void handleSocketData<std::basic_string<char>>(const std::basic_string<char> &t) {
        <recovery-expr>(this->handleData, t);
    }
    void handleData(int data) {
        std::cout << data << std::endl;
    }
};
class B : public A {
public:
    void handleData(std::string data) const {
        std::cout << data << std::endl;
    }
};
int main(int argc, char *argv[]) {
    A a;
    B b;
    a.handleSocketData<int>(30);
    b.handleSocketData<std::string>("Hi");
    return 0;
}

So right now I have a compilation error, saying that no function handleData(const std::string& data) is found, which is normal.

A workaround we've found is to define a two-arguments template, taking the daughter class as argument (kind of visitor pattern) :

#include <iostream>

class A
{
public:
    template <typename T, typename U>
    void handleSocketData(U& u, const T& t)
    {
        u.handleData(t);
    }

    void handleData(int data)
    {
        std::cout << data << std::endl;
    }

};

class B: public A
{
public :
    void handleData(std::string data)
    {
        std::cout << data << std::endl;
    }
};

int main(int argc, char *argv[])
{
    A a;
    B b;
    a.handleSocketData<int>(a, 30);
    b.handleSocketData<std::string>(b, "Hi");
    return 0;
}

What do you think ? Is there a cleaner way ?

CodePudding user response:

This looks like a classic use case for CRTP. You can make A a template over a derived class Derived and then dispatch function calls to the derived class via a static_cast. For this to work, any derived class Derived must be derived from A<Derived>.

Since you seem to want to use A as a non-abstract class, you would have to add a default type for Derived marking it as final, i.e. nothing derives from it. In the following code, the empyt struct FinalTag serves this purpose.

The private cast method casts the this pointer to a reference of Derived, if A is not final, or to a reference of A if it is final. You can use if constexpr to make sure that this distinction is performed at compile-time and you have no runtime overhead.

#include <iostream>

struct FinalTag {};

template <typename Derived=FinalTag>
class A
{
public:
    template <typename T>
    void handleSocketData(const T& t)
    {
        cast().handleData(t);
    }

    void handleData(int data)
    {
        std::cout << data << std::endl;
    }

private:
    constexpr auto& cast() {
        if constexpr (!std::is_same_v<Derived, FinalTag>) {
            return static_cast<Derived&>(*this);
        }
        else {
            return *this;
        }
    }

};

class B: public A<B>
{
public :
    using Base = A<B>;
    using Base::handleData;

    void handleData(std::string data)
    {
        std::cout << data << std::endl;
    }
};

int main(int argc, char *argv[])
{
    A a;
    B b;
    a.handleSocketData(30);
    b.handleSocketData("Hi");

    // this only works if you bring in Base::handleData in the 
    // derived class
    b.handleSocketData(30);
    return 0;
}

Live Code: https://godbolt.org/z/Y4s73nW1G

This is a prototype. You would want to add a const version to the cast method for instance.

CodePudding user response:

A slightly different version of CRTP to the one suggested by Joerg Brech could be more suitable in some cases.

#include <iostream>

class A
{
public:
    template <typename T, typename Class>
    void handleSocketData(const T& t)
    {
        static_cast<Class*>(this)->handleData(t);
    }

    void handleData(int data)
    {
        std::cout << data << std::endl;
    }

};

class B: public A
{
public :
    void handleData(std::string data) const
    {
        std::cout << data << std::endl;
    }
};

int main(int argc, char *argv[])
{
    A a;
    B b;
    a.handleSocketData<int, A>(30);
    b.handleSocketData<std::string, B>("Hi");
    return 0;
}

It is very similar to your solution in the sense that we instruct handleSocketData which class it should use to call handleData from. The only difference is that the decision is made not dynamically but at compile time.

  • Related