0

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.

  • `class ClassMethod` just use `std::function` with `std::bind`. `Then the problem arises here:` Please show the definition of `SimpleBorder`. `typename ReturnType` that's strange template, you already know the return type - it's in `void* methodAddress`. Just instead of `void*` actually use the function type. You want your reflection to guess the type of the function when calling? `would make it unable to be saved in the map.` Yes, but it also makes you unable to call it. Where from do you want to know what type your want to call? Maybe use `std::variant` and enumerate possible types? – KamilCuk Feb 19 '21 at 18:09
  • void* methodAddress is not the return type. It was an attempt to cast the method to a "generic type", from which I could cast it back to whatever is specified in the Invoke template. I Also added the whole definition of the class. I don't want the reflection to guess, it is the job of the user to "Guess". He will have tools to obtain the meta data about the method from elsewhere, so it wouldn't be guessing. – Alma Lopez Feb 19 '21 at 18:22
  • Note that casting function pointer to `void*` may be invalid. You should never convert a function pointer to a normal pointer - use like a `void(*)()` pointer instead. See ex. https://stackoverflow.com/questions/31482624/is-there-such-a-thing-as-a-generic-function-pointer-in-c-that-can-be-assigned-ca . You did not post `SimpleBorder` declaration - `class SimpleBorder : something { seomthing } `? – KamilCuk Feb 19 '21 at 18:28
  • `it is the job of the user to "Guess".` So if he knows, why doesn't he like `methodTable["SetColor"].Invoke(Gdiplus::Color::Red)`? Please show an [MCVE]. `just a failed attempt at making this work` What fails exactly? Do you get a compiler error? A runtime error? Please post the error verbatim. `inline ReturnType ClassMethod::Invoke` You should either remove `inline`, or add that `inline` to structure definition. – KamilCuk Feb 19 '21 at 18:31
  • and + these out of class definitions for templates are making it hard. Why not move function definitians inside class template? `template class B { template void func() { something here; }}` is simpler then ``template class B { template void func(); }` followed by ``template template void func() {something_here;}`? – KamilCuk Feb 19 '21 at 18:38
  • What fails is the cast from `(void*)(&SimpleBorder::SetColor)` to void. You said yourself that it may be invalid and pretty much answered the question you asked yourself. `methodTable["SetColor"].Invoke(Gdiplus::Color::Red)` is completely fine, the problem is storing the methods inside the table without being strictly of a single type. Neither function nor bind allows you to do that. During the construction almost nothing except the method owner should be specified, during the invoke you have to specify the arguments and the return type of the method. – Alma Lopez Feb 19 '21 at 18:49
  • [`std::bind`](https://en.cppreference.com/w/cpp/utility/functional/bind) will allow to wrap a method so it can be passed around like a [`std::function`](https://en.cppreference.com/w/cpp/utility/functional/function), but this approach means it's already bound to an object. If you want to apply to arbitrary objects, then look at C++ [pointer to member](https://en.cppreference.com/w/cpp/language/pointer#Pointers_to_members) types/operator. – Perette Feb 19 '21 at 18:52

2 Answers2

1

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.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • That is a really nice solution. Thanks a lot. Could you please further explain how void (T::*methodAddress)(); behaves? Is it the same as specifying a void* pointer for a static function? – Alma Lopez Feb 19 '21 at 18:56
  • Read the first 3 sentences of the answer on https://stackoverflow.com/questions/31482624/is-there-such-a-thing-as-a-generic-function-pointer-in-c-that-can-be-assigned-ca . But, anyway, you will not be able to find a system where a function pointer has less bits then a `void*` pointer, so using `void*` is anyway safe and ok - I believe it's a stylistic measure, `void*` represents pointer to data. Function pointer represents a pointer to function. There is http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2230.htm , will see if it gets to C, maybe it will get to C++. – KamilCuk Feb 19 '21 at 18:57
  • Yes, thanks, but the problem seems to be related to C styled functions, not C++ method function pointers. I thought that C++ standard forbids for method function pointers to be recast. – Alma Lopez Feb 19 '21 at 18:59
  • `I thought` Yes, I actually think that too... well, I did not research that much before writing this code - so that's on you :p – KamilCuk Feb 19 '21 at 19:00
0

I tested this with static functions and it worked.

As someone interested in going with the C++ standard and avoiding hacks, you should be aware that "it worked" does not mean "it worked without disobeying the C++ standard." In this case, you have obtained pointers to void from pointers to functions. You did this via a C-style cast expression, which in this case follows the rules for reinterpret_cast. The relevant rule is #8 in cppreference's list (emphasis added):

  1. On some implementations (in particular, on any POSIX compatible system as required by dlsym), a function pointer can be converted to void* or any other object pointer, or vice versa. If the implementation supports conversion in both directions, conversion to the original type yields the original value, otherwise the resulting pointer cannot be dereferenced or called safely.

The standard does support your current approach for static member functions, but only on some systems. If you are concerned about your code breaking after a compiler upgrade, your approach is probably fine. If you are concerned about portability across systems, though, your initial approach is already flawed.

Expanding the approach to non-static member functions is even more problematic. The options provided by the standard for what a pointer to member function can be converted to are even more limited: any pointer-to-member-function type (point 10, for those still reading the cppreference page). Type erasure is not a promising approach (although it could be done, as in the other answer). I would be inclined to try working with function objects (functionals) instead of function pointers, but that would be the subject for another answer.

As a rule of thumb, if you introduce something with the type void*, your approach is probably more from a C mindset than a C++ mindset.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • `As a rule of thumb, if you introduce something with the type void*, your approach is probably more from a C mindset than a C++ mindset.` I have to disagree, as if you are implementing reflection --- something that C++ does not support natively, you are simply forced to interpret on the memory level. I don't see a way of implementing reflection for methods if you are working on the level of objects. – Alma Lopez Feb 20 '21 at 22:06
  • @AlmaLopez It is incorrect, yet common, to conclude "cannot be done" from "I don't see a way". – JaMiT Feb 20 '21 at 23:31