Nov 11, 2023

Calling C++ class methods by its string name

Consider the following cases:
  1. You have an abstract class that you use as an interface for other classes deriving from it. The abstract class just have the bare minimum methods for interacting with it or none at all, derived classes can define new methods, you can only get access to the derived class object by the abstract class. You want to call a method in the derived class from the abstract class.
  2. You have a class where you want to add new methods dynamically, but it's definitions are unknown.
While working in my own project I had reached to this problematic. Also, if you are a Qt developer, it may sound familiar, and this because its what QMetaObject::invokeMethod() does, but I wanted to use that functionality without using Qt.

The struct passing approach


This is the simplest solution that consist in passing the arguments of the function as a classical C struct. I have choosen this solution first because it can be easily adapted to C by replacing std::function with a function pointer.
// The class only contains the bare minimum required methods
class BaseClass
{
    public:
        /* In our case, only the call method will be public,
         * and it will be used to call functions in the derived classes.
         * The methods are called by it's string name, and since the number
         * and types of arguments are unknown, we pass them as void *.
         * Continue reading for understanding how to pass the arguments.
         */
        inline bool call(const std::string &key, void *args=nullptr) const
        {
            for (auto it = this->m_functions.begin(); it != this->m_functions.end(); it++)
                if (it->first == key) {
                    it->second(args);

                    return true;
                }

            return false;
        }

    private:
        // Store all our dynamically callable methods in a map
        std::map<std::string, std::function<void (void *)>> m_functions;

    protected:
        // Store the functions by name and std::function.
        inline void registerMethod(const std::string &key, const std::function<void (void *)> &method)
        {
            this->m_functions[key] = method;
        }
};

/* 'Echo' class can be constructed,
 * but can only be called from BaseClass
 */
class Echo: public BaseClass
{
    public:
        Echo()
        {
            /* Preferably, register all methods in the constructor,
             * but in practical, but you can register.
             */
            this->registerMethods();
        }

        /* This method is inaccessible from BaseClass,
         * yet we want to call it from there.
         */
        void echo(const std::string &str) const
        {
            std::cout << str << std::endl;
        }

    private:
        inline void registerMethods()
        {
            /* We then wrap the 'echo()' in a std::function,
             * and register it.
             */
            this->registerMethod("echo", [this] (void *args) {
                /* And here is the trick for passing the arguments
                 * to the method, we create a struct (the name of
                 * the struct is irrelevant and won't be public),
                 * and each member of the struct should be in the
                 * same order of the arguments of the method, with
                 * the same types.
                 */
                struct FuncArgs
                {
                    std::string str;
                };

                /* Then, do a type casting of the 'void *' to the
                 * defined struct.
                 */
                auto funcArgs = reinterpret_cast<FuncArgs *>(args);

                /* And finally call the function with it's arguments.
                 */
                this->echo(funcArgs->str);
            });
        }
};
And then we call the dynamic methods as follow:
int main()
{
    /* This is the real object derived from 
     * the 'Echo' class.
     */
    Echo obj;
    
    /* Get a pointer to the object and cast it 
     * to it's base class
     */
    auto drv = static_cast<BaseClass *>(&obj);

    /* Create a struct for passing the arguments 
     * in the types and order expected.
     * The name of the struct i irrelevant, so 
     * we make it's type name anonymous.
     */
    struct
    {
        std::string str {"Hello world!"};
    } args;
    
    // Call the method and pass it's arguments struct.
    drv->call("echo", &args);

    // Also call the real function for testing purposes.
    obj.echo("Goodby world :^(");

    return 0;
}
If the method does not receive any argument, then ignore creating any argument struct and call it directly. If the method returns an argument, pass the result as a member of the struct, preferably at the top of it.

The std::vector<std::any> passing approach


And this is a more modern approach using the latest features of C++.
// The class only contains the bare minimum required methods
class BaseClass
{
    public:
        /* In this case we can separate into input and output arguments.
         * By convention, output arguments go first;
         */
        inline bool call(const std::string &key,
                         std::vector<std::any> *result=nullptr,
                         const std::vector<std::any> &args={}) const
        {
            for (auto it = this->m_functions.begin(); it != this->m_functions.end(); it++)
                if (it->first == key) {
                    it->second(result, args);

                    return true;
                }

            return false;
        }

        // A handy method if we want to ignore the returning arguments
        inline bool call(const std::string &key,
                         const std::vector<std::any> &args) const
        {
            return this->call(key, nullptr, args);
        }

    private:
        // Store all our dynamically callable methods in a map
        std::map<std::string, 
                    std::function<void (std::vector<std::any> *,
                                           const std::vector<std::any> &)>> m_functions;

    protected:
        // Store the functions by name and std::function.
        inline void registerMethod(const std::string &key, 
                                   const std::function<void (std::vector<std::any> *,
                                                                const std::vector<std::any> &)> &method)
        {
            this->m_functions[key] = method;
        }
};

/* 'Echo' class can be constructed,
 * but can only be called from BaseClass
 */
class Echo: public BaseClass
{
    public:
        Echo()
        {
            /* Preferably, register all methods in the constructor,
             * but in practical, but you can register.
             */
            this->registerMethods();
        }

        /* This method is inaccessible from BaseClass,
         * yet we want to call it from there.
         */
        void echo(const std::string &str) const
        {
            std::cout << str << std::endl;
        }

        // Use this maethod to test the returning argument
        int sum(int a, int b) const
        {
            return a + b;
        }

    private:
        inline void registerMethods()
        {
            /* We then wrap the 'echo()' in a std::function,
             * and register it.
             */
            this->registerMethod("echo", [this] (std::vector<std::any> *result,
                                                 const std::vector<std::any> &args) {
                /* Here, you simply cast the arguments to the same 
                 * type and in the same original order.
                 */
                this->echo(std::any_cast<std::string>(args[0]));
            });
            this->registerMethod("sum", [this] (std::vector<std::any> *result,
                                                 const std::vector<std::any> &args) {
                /* Here's an example on how to return the value 
                 * returned from the function.
                 */
                auto r = this->sum(std::any_cast<int>(args[0]),
                                   std::any_cast<int>(args[1]));

                if (result)
                    *result = {r};
            });
        }
};
And then we call the dynamic methods as follow:
int main()
{
    /* This is the real object derived from
     * the 'Echo' class.
     */
    Echo obj;

    /* Get a pointer to the object and cast it
     * to it's base class
     */
    auto drv = static_cast<BaseClass *>(&obj);

    // Calling the method is much simpler than before.
    drv->call("echo", {std::string("Hello world!")});

    // Call the method and pass it's arguments struct.
    static const int a = 12;
    static const int b = 34;
    std::vector<std::any> r;
    drv->call("sum", &r, {a, b});
    std::cout << 
              a 
              << 
              " + " 
              << 
              b 
              << 
              " = " 
              << std::any_cast<int>(r[0]) 
              << 
              std::endl;

    return 0;
}

No comments:

Post a Comment