Search code examples
c++metaprogrammingtemplate-meta-programming

Calling a member function by address (C++ Reflection attempt)


Problem description:

Hello. I am implementing my own reflection system for C++. I want to make a simplified system where you can look up a method in a hashtable and call it via template.

My attempt at solving the problem is providing a meta class that contains the information regarding the methods. The information are the address of the method and the Type to which the method belongs to as well as the name. The class also provides an Invoke method, which is a template method where the user defines the argument types based on the prototype.

Problem is that I am currently incapable of finding a way to pass the address of the method into the argument in a way that it would meet C++ rules and keep the code portable.

Current solutions

#pragma once
#include <string>

class Primitive
{
private:
    std::string name;
public:
    Primitive(std::string name);
};


template<class Type>
class ClassMethod : Primitive
{
private:
    void* methodAddress;
    Type* targetClass;
public:
    ClassMethod(std::string name, Type* instance, void* methodAddress);
    template<typename ReturnType, typename ... Args>
    ReturnType Invoke(Args ... args);
};

template<class Type>
ClassMethod<Type>::ClassMethod(std::string name, Type* instance, void* methodAddress) : Primitive(name)
{
    targetClass = instance;
    this->methodAddress = methodAddress;
}

template<class Type>
template<typename ReturnType, typename ...Args>
inline ReturnType ClassMethod<Type>::Invoke(Args ... args)
{
    ReturnType(*funcionPointer)(Args ...) = (ReturnType(*) (Args ...))methodAddress;
    return (*targetClass.*funcionPointer)(args ...);
}

Then the problem arises here:

SimpleBorder::SimpleBorder() : renderBehavior(*this), setColorMethod(std::string("SetColor"), this, (void*)(&SimpleBorder::SetColor))
{
    brush = new Gdiplus::SolidBrush(Gdiplus::Color::White);
    pen = new Gdiplus::Pen(brush);
    pen->SetWidth(0.01f);
}


void SimpleBorder::SetColor(Gdiplus::Color color)
{
    this->color = color;
    pen->SetColor(color);
}

Gdiplus::Color SimpleBorder::GetColor()
{
    return color;
}

void SimpleBorder::SetBorderStyle(Gdiplus::DashStyle style)
{
    pen->SetDashStyle(style);
}

void SimpleBorder::SetThickness(float thickness)
{
    pen->SetWidth(thickness / 100);
}

void SimpleBorder::OnRender(RenderEventInfo e)
{
    e.GetGraphics()->DrawRectangle(pen, 0.0f, 0.0f, 1.0f, 1.0f);
    renderBehavior.OnRender(e);
}

void SimpleBorder::Repaint()
{

}

void SimpleBorder::AddRenderable(Renderable& renderable)
{
    renderBehavior.AddRenderable(renderable);
}

void SimpleBorder::RemoveRenderable(Renderable& renderable)
{
    renderBehavior.RemoveRenderable(renderable);
}

std::vector<std::reference_wrapper<Renderable>> SimpleBorder::GetRenderables()
{
    return renderBehavior.GetRenderables();
}

void SimpleBorder::SetProperty(std::string propertyName, std::string values ...)
{
}

bool SimpleBorder::HasProperty(std::string propertyName)
{
    return false;
}


Declaration

class SimpleBorder : public Renderable, public AccessTool
{
private:
    //Field map
    Gdiplus::SolidBrush* brush;
    Gdiplus::Pen* pen;
    DefaultRender renderBehavior;
    Gdiplus::Color color;

public:
    SimpleBorder(); 
    ~SimpleBorder();
    void SetColor(Gdiplus::Color color);
    Gdiplus::Color GetColor();
    void SetBorderStyle(Gdiplus::DashStyle style);
    void SetThickness(float thickness);

    // Inherited via Renderable
    virtual void OnRender(RenderEventInfo e) override;
    virtual void Repaint() override;
    virtual void AddRenderable(Renderable& renderable) override;
    virtual void RemoveRenderable(Renderable& renderable) override;
    virtual std::vector<std::reference_wrapper<Renderable>> GetRenderables() override;

    // Inherited via Renderable
    virtual void SetProperty(std::string propertyName, std::string values ...) override;
    virtual bool HasProperty(std::string propertyName) override;
    virtual std::any GetProperty(std::string propertyName) override;
};

Solution Description:

As you can see the idea of the template ClassMethod is to have it purely based on the type of the class it belongs to, in order to be able to save it in a std::unordered_map<std::string, ClassMethod<SimpleBorder>> methodTable and then invoke calls like this:

methodTable["SetColor"].Invoke(Gdiplus::Color::Red)

which is the reason why I am avoiding a class template which would define the method arguments, as it would create tight coupling between the method and would make it unable to be saved in the map.

The idea is that the map would contain the methods, and it is the job of the user to get the proper arguments and insert them into the Invoke.

What I tried:

The code in the Invoke is more or less just a failed attempt at making this work. I tested this with static functions and it worked. The problem arises with member functions as they are saved differently. I found a few hacks on other questions, but the problem is those solutions don't go by the C++ standard and they can stop working at any time.

What I need:

What I would like is either an answer which would show me or tell me a different approach where this would also work with member functions, or an answer that would edit the solution I have, in a way that it would work without disobeying the C++ standard.


Solution

  • Grab this:

    #include <string>
    
    class Primitive
    {
    private:
        std::string name;
    public:
        Primitive(std::string name) : name(name) {}
    };
    
    template<typename T>
    class ClassMethod : Primitive
    {
    private:
        void (T::*methodAddress)(); // used as a "generic" function pointer
        T& targetClass;
    public:
        template<typename R, typename ...Args> // no need for casting to void*
        // and also check type - `methodAddress` is a member function specifically on `T`, not any abstract pointer...
        ClassMethod(std::string name, T& instance, R (T::*methodAddress)(Args...)) :
            Primitive(name),
            // take a reference, unless you accept `nullptr`... I do not believe you do...
            targetClass(instance),
            // I do not believe this is very valid, but "will work".
            methodAddress(reinterpret_cast<void(T::*)()>(methodAddress))
            {
                // all those out of class template deifinitions are confusing
                // let's move them inside!
            }
    
        template<typename ReturnType, typename ...Args>
        ReturnType Invoke(Args... args)
        {
            ReturnType (T::*funcionPointer)(Args...) = reinterpret_cast<ReturnType (T::*)(Args...)>(methodAddress);
            // forward arguments
            return (targetClass.*funcionPointer)(std::forward<Args...>(args)...);
            // when targetClass is a pointer, then
            // `*targetClass.*funcionPointer` is equal to `*(targetClass.*funcionPointer)` !!
            // you want `((*targetClass).*functionPointer)` or just `targetClass->*funcionPointer
        }
    };
    
    // abstract example
    
    #include <iostream>
    #include <vector>
    
    struct A {
        double func(int a) {
            std::cout << "func(" << a << ") ";
            return 3.41;
        }
        int fun2(char b) {
            std::cout << "fun2('" << b << "') ";
            return 50;
        }
    };
    
    int main() {
        A a;
        std::vector<ClassMethod<A>> v;
        v.emplace_back("func", a, &A::func);
        v.emplace_back("fun2", a, &A::fun2);
        auto rd = v[0].Invoke<double>(5); // you have to specify return type
        auto ri = v[1].Invoke<int>('a'); // you have to specify return type
        std::cout << rd << " " << ri << "\n";
    }
    

    tested on godbold prints:

    func(5) fun2('a') 3.41 50
    

    The function definitions outside of class ClassMethod definition are just confusing, with those template<> template<> stuff. Just put them inside class definition.