简体   繁体   中英

Map of function pointers to member functions

I've tried various solutions on SO to solve this problem, yet I must be doing something wrong.

I have several classes where methods in each of the classes have the same method signature:

typedef int (*ControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> &params);

And an example class having some method using that signature:

class StaticContentController {
public:
    int handleStaticContentRequest(const std::string &data, const std::unordered_map<std::string, std::string> &params) {
        return 1;
    }
}

Now I try to create a map of pointers to member functions:

std::map<std::string, ControllerMethod> operations;
        operations.emplace("staticContent", &StaticContentController::handleStaticContentRequest);
std::string d("test.txt");
ControllerMethod f = operations["staticContent"];
auto s = ((_staticContentController).*f)(d, pooledQueries); // <- compile error here

but calling the method gives the compile error

Right hand operand to .* has non-pointer-to-member type 'web::server::ControllerMethod'

What am I missing?

Update:

I now have an empty Controller base class which other controller classes inherit from:

namespace web { namespace server {

    class Controller {

    };

    typedef ControllerResponse (Controller::*ControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> &params);

}}

Now I'm getting the following error at operations.emplace():

No matching constructor for initialization of 'std::__1::pair<const std::__1::basic_string<char>, web::server::ControllerResponse 

Updated answer

You're trying to use two different paradigms here, and they aren't really compatible with one another. If I interpret your edit correctly, you're trying to create a map of functions that call into other classes, and you want to declare this map as a set of function pointers.

Function pointers are an assembly level construct, exposed by C. The syntax reflects this - and getting a C++ class to conform to this is not possible without help - namely, adding a context pointer parameter that is associated with every function pointer, and converting the context pointer to a class instance to call the member function.

So, how do we fix the problem?

In both of the next approaches, we need a context object associated with the function table. This involves creating a structure to hold the member function and the context:

template<typename T> struct FunctionTableEntry
{
    ControllerMethod Function;
    T* Context;
};

and our function pointer becomes the following:

typedef ControllerResponse (T::*ControllerMethod)(const std::string &data, const StringMap &params);

Here, StringMap is a typedef for std::unordered_map<std::string, std::string> .

Our main problem now comes with removing the template parameter T, as we can't make maps of runtime defined templates (a template who's type will only be known at run time).

There are two main approaches to take in resolving this, and both have issues that will need to be considered. The first is to perform C style type erasure with pointers and very careful association. The second is to abandon function pointers in favor of C++ function objects.

C-Style Type Erasure

This option involves using C-style casts to convert the class instance pointer to its base class type, the member function pointer to the type expected by the function declaration, and then making the call as though the base class defines the method. This requires the use of pointers, and cannot be done without them.

To do this, our FunctionTableEntry structure changes to the following:

struct FunctionTableEntry
{
    ControllerMethod Function;
    Controller* Context;
}

and our function pointer to:

typedef ControllerResponse (Controller::*ControllerMethod)(const std::string &data, const StringMap &params);

To add a new entry, we do the following:

std::map<std::string, FunctionTableEntry> operations;
FunctionTableEntry Entry;
Entry.Function = (ControllerMethod)&StaticContentController::handleStaticContentRequest;
Entry.Context = (Controller*)&_staticContentController;
operations.emplace("staticContent", Entry);

And to call it:

FunctionTableEntry f = operations["staticContent"];
auto s = ((f.Context)->*f.Function)(d, pooledQueries);

This method suffers from a few drawbacks - first, you have no other choice but to use pointers to refer to your controller objects - casting will not function properly otherwise. You can make this a bit more C++ friendly with std::shared_ptr, but otherwise, there is no way to replace it. This also means you need to carefully manage the lifetime of your controller objects. If they get freed while the function table is still referencing them you will almost certainly crash the system.

Second, the casting can cause issues with complex inheritance hierarchies. This method only works if (Controller*)_staticContentController == _staticContentController , ie casting to the base class gives the same numerical pointer value. Otherwise, the called method will fail as it will not be able to properly reference its local data.

This method has the advantage of being quite fast, however. There is no function overhead besides the table lookup, and the generated assembly is not much more than just calling the function normally. It is also runtime independent - so long as the equality expression above is true with all users of the controller system, anyone with a C++ compiler can create a new controller and this system will be able to call their functions, even if they use a completely different runtime library.

Additionally, if you know the controller instance is going to be used with multiple functions, you can modify the structure to provide a map of functions associated with one Context value, allowing you to reduce some of the memory overhead. This may not be possible with your design, but it's worth looking into if memory is a concern.

C++ Function Objects

The second solution is to completely do away with C-style function pointers altogether and use std::function . Since std::function can contain instance data as part of itself, and can be placed into a map, this allows you to std::bind a member function, creating a partially specified function call (I believe in functional programming this is what's called a closure).

In this case, there is no FunctionTableEntry structure - instead we use the following:

typedef std::function<ControllerResponse(const std::string&, const StringMap&)> ControllerMethod;

To add a new method, we do the following:

std::map<std::string, ControllerMethod> operations;
operations.emplace("staticContent", std::bind(&StaticContextController::handleStaticContentRequest, &_staticContentController, std::placeholders::_1, std::placeholders::_2);

This creates a closure that calls the member function with the required controller instance.

To call this, we do the following:

std::string d("test.txt");
ControllerMethod f = operations["staticContent"];
auto s = f(d, pooledQueries);

C++ function objects override operator () , which allows them to work as though they were static functions.

This method allows for both member functions and static functions to exist in the same map. It also allows for complex inheritance hierarchies to occur, as there is no casting to make things function - everything occurs with template functions.

The downside to this method is you still need to deal with object lifespan - the content controller objects cannot be destroyed until after the function map has been cleared. In addition, there is some overhead due to the use of std::function with placeholder parameters (though that likely depends on the runtime library in use, my tests have shown it generates a whole lot more code in x86-64 GCC 9.3).

This method also is not runtime independent - whatever runtime you choose to use here must also be used by every programmer that uses this code, otherwise incompatibilities in the way each library creates and stores std::function will cause strange failures. This means no compiler mixing - if you used MSVC 2019 to build the API, everyone else who uses this library must use MSVC2019 to build their controller component. If you aren't providing an API here, then this is not an issue.


Original answer

Your function pointer declaration is wrong - pointers to members have a different syntax to the normal function pointer typedef.

A normal function pointer uses the syntax you have currently:

typedef int (*foo)(int x, int y);

A pointer to member function typedef looks like this:

typedef int (SomeClass::*foo)(int x, int y);

The SomeClass:: section is required as pointers to members have an additional parameter to them, called this . In C++, the this pointer is passed as the first argument to the function, which makes the function declaration different (as the actual assembly code needed to call the function is different, see MSVC generated assembly for a real world example).

To solve the issue, you need to provide a base class that can be used to declare the typedef, then inherit from that class to allow the method to be called. This is effectively identical to using inheritance, unless you have multiple methods in the same type that have the same signature, but do different things.

The DirectX 11 Effects framework uses this exact paradigm to avoid branching when configuring different shader types in the graphics pipeline - see here , at line 590.

As pointed out, the type of a non-static member function of the class StaticContentController is not:

typedef int (*ControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> &params);

Instead, it is:

typedef int (StaticContentController::*StaticContentControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> &params);

This was your initial error.

This makes sense as you need an instance to call the member function, and the instance has a type as well. And it makes sense that if you have a Base::*Function pointer, you can call it with an instance of a class publicly and unambiguously derived from Base, because a derived pointer can be converted implicitly to a base pointer.

It also makes sense that you cannot assign a Derived::*Function pointer to a Base::*Function pointer because the result could be called with any Base instance, which need not be a Derived instance. This was the error in the question update.

In this very limited circumstance, C++ behaves completely logically.

With the modification to the correct type, your snippet will compile:

std::map<std::string, StaticContentControllerMethod> operations;
operations.emplace("staticContent", 
    &StaticContentController::handleStaticContentRequest);
std::string d("test.txt");
StaticContentControllerMethod f = operations["staticContent"];
auto s = ((_staticContentController).*f)(d, pooledQueries); // <- works

So presumably your actual question is how to store in this map member function pointers for multiple classes and not just StaticContentController . But that is the wrong question. You have to have the instance ( _staticContentController ) to invoke the member function pointer, so you already know the type.

So maybe you want to ask how to erase the type. One way is storing something that doesn't require an instance: for that, use std::function as the mapped type and bind the instance when inserting into the map. That would work and be straightforward if you have the controller at the time the map is created. A second way is using a type erasing type like std::any for the mapped type, and use any_cast at the point of use to return it to its initial type. A third way is to use a common base class with virtual functions which are overridden in your classes. Since the virtual functions can be called with a base pointer, you can store member function pointers of the base class.

Alternatively, maybe you want to ask how to have a type-indexed collection: the type is known at lookup time (because you have an instance) and you need to lookup a value whose type (member function pointer) depends on the "key" type.

The simplest way to do this is to have templated classes, and let the compiler handle the mapping:

template<typename T>
struct operations {
    static std::map<std::string, void (T::*)(etc.)> pointers;
};
// use:
operations<StaticContentController>::pointers["staticContent"];

Another version of type-indexing might have the following interface:

template<template<typename> typename Value>
class type_keyed_map
{
public:
    template<typename T>
    void insert(std::unique_ptr<Value<T>> value);

    template<typename T>
    auto find() -> Value<T>*; // return null if not found
};

You can use a std::map in the implementation, but std::map does not allow multiple value types.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM