在 C++ 中动态创建和调用类方法的最简单方法是什么?

2021-12-21 00:00:00 object c++ model-view-controller

我想用类名和方法、唯一标识符和指向方法的指针填充映射.

I want to fill a map with class name and method, a unique identifier and a pointer to the method.

typedef std::map<std::string, std::string, std::string, int> actions_type;
typedef actions_type::iterator actions_iterator;

actions_type actions;
actions.insert(make_pair(class_name, attribute_name, identifier, method_pointer));

//after which I want call the appropriate method in the loop

while (the_app_is_running)
{
    std::string requested_class = get_requested_class();
    std::string requested_method = get_requested_method();

    //determine class
    for(actions_iterator ita = actions.begin(); ita != actions.end(); ++ita)
    {
        if (ita->first == requested_class && ita->second == requested_method)
        {
            //class and method match
            //create a new class instance
            //call method
        }
    }
}

如果方法是静态的,那么一个简单的指针就足够了,问题很简单,但我想动态创建对象,所以我需要存储一个指向类的指针和方法的偏移量,我不知道这是否有效(如果偏移量总是相同等).

If the method is static then a simple pointer is enough and the problem is simple, but I want to dynamically create the object so I need to store a pointer to class and an offset for the method and I don't know if this works (if the offset is always the same etc).

问题在于 C++ 缺乏反射,在解释性语言中,有反射的等效代码应该是这样的(PHP 中的例子):

The problem is that C++ lacks reflection, the equivalent code in a interpreted language with reflection should look like this (example in PHP):

$actions = array
(
     "first_identifier" => array("Class1","method1"),
     "second_identifier" => array("Class2","method2"),
     "third_identifier" => array("Class3","method3")
);

while ($the_app_is_running)
{
     $id = get_identifier();

     foreach($actions as $identifier => $action)
     {
         if ($id == $identifier)
         {
             $className = $action[0];
             $methodName = $action[1];

             $object = new $className() ;

             $method = new ReflectionMethod($className , $methodName);
             $method -> invoke($object);    
         }
     }
 }

PS:是的,我正在尝试用 C++ 制作一个(网络)MVC 前端控制器.我知道我知道为什么不使用 PHP、Ruby、Python(在此处插入您最喜欢的网络语言)等?我只想要 C++.

PS: Yes I'm trying to make a (web) MVC front controller in C++. I know I know why don't use PHP, Ruby, Python (insert your favorite web language here) etc?, I just want C++.

推荐答案

我在上几个小时写了这些东西,并将它添加到我的有用东西集合中.最困难的是处理工厂函数,如果你想创建的类型没有任何关系.为此,我使用了 boost::variant.你必须给它一组你想要使用的类型.然后它将跟踪变体中当前的活动"类型是什么.(boost::variant 是所谓的歧视联合).第二个问题是如何存储函数指针.问题是指向A 成员的指针不能存储到指向B 成员的指针.这些类型是不兼容的.为了解决这个问题,我将函数指针存储在一个重载其 operator() 并采用 boost::variant:

I wrote that stuff last hours, and added it to my collection of useful stuff. The most difficult thing is to cope with the factory function, if the types you want to create are not related in any way. I used a boost::variant for this. You have to give it a set of types you ever want to use. Then it will keep track what is the current "active" type in the variant. (boost::variant is a so-called discriminated union). The second problem is how you store your function pointers. The problem is that a pointer to a member of A can't be stored to a pointer to a member of B. Those types are incompatible. To solve this, i store the function pointers in an object that overloads its operator() and takes a boost::variant:

return_type operator()(variant<possible types...>)

当然,所有类型的函数都必须具有相同的返回类型.否则整个游戏就没有什么意义了.现在代码:

Of course, all your types' functions have to have the same return type. Otherwise the whole game would only make little sense. Now the code:

#include <boost/variant.hpp>
#include <boost/function.hpp>
#include <boost/bind.hpp>
#include <boost/tuple/tuple.hpp>
#include <boost/mpl/identity.hpp>
#include <boost/function_types/parameter_types.hpp>
#include <boost/function_types/result_type.hpp>
#include <boost/function_types/function_arity.hpp>
#include <boost/preprocessor/repetition.hpp>
#include <map>
#include <string>
#include <iostream>

// three totally unrelated classes
// 
struct foo {
    std::string one() {
        return "I ";
    }
};

struct bar {
    std::string two() {
        return "am ";
    }
};

struct baz {
    std::string three() const {
        return "happy!";
    }
};

// The following are the parameters you have to set
//

// return type
typedef std::string return_type;
// variant storing an object. It contains the list of possible types you
// can store.
typedef boost::variant< foo, bar, baz > variant_type;
// type used to call a function on the object currently active in
// the given variant
typedef boost::function<return_type (variant_type&)> variant_call_type;

// returned variant will know what type is stored. C++ got no reflection, 
// so we have to have a function that returns the correct type based on
// compile time knowledge (here it's the template parameter)
template<typename Class>
variant_type factory() {
    return Class();
}

namespace detail {
namespace fn = boost::function_types;
namespace mpl = boost::mpl;

// transforms T to a boost::bind
template<typename T>
struct build_caller {
    // type of this pointer, pointer removed, possibly cv qualified. 
    typedef typename mpl::at_c<
        fn::parameter_types< T, mpl::identity<mpl::_> >,
        0>::type actual_type;

    // type of boost::get we use
    typedef actual_type& (*get_type)(variant_type&);

// prints _2 if n is 0
#define PLACEHOLDER_print(z, n, unused) BOOST_PP_CAT(_, BOOST_PP_ADD(n, 2))
#define GET_print(z, n, unused)                                         
    template<typename U>                                                
    static variant_call_type get(                                       
        typename boost::enable_if_c<fn::function_arity<U>::value ==     
            BOOST_PP_INC(n), U>::type t                                 
        ) {                                                             
        /* (boost::get<actual_type>(some_variant).*t)(n1,...,nN) */     
        return boost::bind(                                             
            t, boost::bind(                                             
                (get_type)&boost::get<actual_type>,                     
                _1) BOOST_PP_ENUM_TRAILING(n, PLACEHOLDER_print, ~)     
            );                                                          
    }

// generate functions for up to 8 parameters
BOOST_PP_REPEAT(9, GET_print, ~)

#undef GET_print
#undef PLACEHOLDER_print

};

}

// incoming type T is a member function type. we return a boost::bind object that
// will call boost::get on the variant passed and calls the member function
template<typename T>
variant_call_type make_caller(T t) {
    return detail::build_caller<T>::template get<T>(t);
}

// actions stuff. maps an id to a class and method.
typedef std::map<std::string, 
                 std::pair< std::string, std::string >
                 > actions_type;

// this map maps (class, method) => (factory, function pointer)
typedef variant_type (*factory_function)();
typedef std::map< std::pair<std::string,      std::string>, 
                  std::pair<factory_function, variant_call_type> 
                  > class_method_map_type;

// this will be our test function. it's supplied with the actions map, 
// and the factory map
std::string test(std::string const& id,
                 actions_type& actions, class_method_map_type& factory) {
    // pair containing the class and method name to call
    std::pair<std::string, std::string> const& class_method =
        actions[id];

    // real code should take the maps by const parameter and use
    // the find function of std::map to lookup the values, and store
    // results of factory lookups. we try to be as short as possible. 
    variant_type v(factory[class_method].first());

    // execute the function associated, giving it the object created
    return factory[class_method].second(v);
}

int main() {
    // possible actions
    actions_type actions;
    actions["first"] = std::make_pair("foo", "one");
    actions["second"] = std::make_pair("bar", "two");
    actions["third"] = std::make_pair("baz", "three");

    // connect the strings to the actual entities. This is the actual
    // heart of everything.
    class_method_map_type factory_map;
    factory_map[actions["first"]] = 
        std::make_pair(&factory<foo>, make_caller(&foo::one));
    factory_map[actions["second"]] = 
        std::make_pair(&factory<bar>, make_caller(&bar::two));
    factory_map[actions["third"]] = 
        std::make_pair(&factory<baz>, make_caller(&baz::three));

    // outputs "I am happy!"
    std::cout << test("first", actions, factory_map)
              << test("second", actions, factory_map)
              << test("third", actions, factory_map) << std::endl;
}

它使用了来自 boost 预处理器、函数类型和绑定库的非常有趣的技术.可能循环复杂,但如果您获得该代码中的键,就不再需要掌握了.如果你想改变参数数量,你只需要调整 variant_call_type:

It uses pretty fun techniques from boost preprocessor, function types and bind library. Might loop complicated, but if you get the keys in that code, it's not much to grasp anymore. If you want to change the parameter count, you just have to tweak variant_call_type:

typedef boost::function<return_type (variant_type&, int)> variant_call_type;

现在您可以调用带有 int 的成员函数.以下是呼叫方的外观:

Now you can call member functions that take an int. Here is how the call side would look:

return factory[class_method].second(v, 42);

玩得开心!

如果你现在说以上太复杂了,我不得不同意你的看法.它很很复杂,因为C++不是真正为这种动态使用而生的.如果可以在要创建的每个对象中对方法进行分组和实现,则可以使用纯虚函数.或者,您可以在默认实现中抛出一些异常(如 std::runtime_error),因此派生类不需要实现所有内容:

If you now say the above is too complicated, i have to agree with you. It is complicated because C++ is not really made for such dynamic use. If you can have your methods grouped and implemented in each object you want create, you can use pure virtual functions. Alternatively, you could throw some exception (like std::runtime_error) in the default implementation, so derived classes do not need to implement everything:

struct my_object {
    typedef std::string return_type;

    virtual ~my_object() { }
    virtual std::string one() { not_implemented(); }
    virtual std::string two() { not_implemented(); }
private:
   void not_implemented() { throw std::runtime_error("not implemented"); }
};

对于创建对象,通常的工厂就可以

For creating objects, a usual factory will do

struct object_factory {
    boost::shared_ptr<my_object> create_instance(std::string const& name) {
        // ...
    }
};

映射可以由映射 ID 到一对类和函数名称(与上面相同)的映射以及映射到 boost::function 的映射组成:

The map could be composed by a map mapping IDs to a pair of class and function name (the same like above), and a map mapping that to a boost::function:

typedef boost::function<my_object::return_type(my_object&)> function_type;
typedef std::map< std::pair<std::string, std::string>, function_type> 
                  class_method_map_type;
class_method_map[actions["first"]] = &my_object::one;
class_method_map[actions["second"]] = &my_object::two;

调用函数的方式如下:

boost::shared_ptr<my_object> p(get_factory().
    create_instance(actions["first"].first));
std::cout << class_method_map[actions["first"]](*p);

当然,使用这种方法,您会失去灵活性和(可能尚未分析)效率,但您可以大大简化您的设计.

Of course, with this approach, you loose flexibility and (possibly, haven't profiled) efficiency, but you greatly simplify your design.

相关文章