简体   繁体   中英

typedef on a templated function with member usage

I have two functions which need to be exposed by the class and they look like this (more will follow):

void print_a(std::string s);
void print_b(std::string s, int val);

"Under the hood" they are doing the same exact thing namely doing a lookup in a map and passing the call parameters to the function pointer retrieved by the map:

#include <stdint.h>
#include <iostream>
#include <string>
#include <map>

class Thing{
private:

    void do_a(){
        std::cout << "hello";
    }
    //there might be also a method do_a_extended() which has a different key in the map

    void do_b(int age){
        std::cout << "my age is " << age;
    }


    typedef void (Thing::*do_stuff_a)();
    typedef void (Thing::*do_stuff_b)(int);

    std::map<std::string, do_stuff_a> a_table;
    std::map<std::string, do_stuff_b> b_table;

public:
    
    void print_a(std::string s){
        do_stuff_a handler = a_table[s];
        if(handler){
            (this->*handler)();
        }
    }

    void print_b(std::string s, int val){
        do_stuff_b handler = b_table[s];
        if(handler){
            (this->*handler)(val);
        }
    }

};

I dislike the fact that there is a lot of boilerplate code involved. I wonder if its possible to pass a member into template so I can do this:

class Thing{
private:

    void do_a(){
        std::cout << "hello";
    }

    void do_b(int age){
        std::cout << "my age is " << age;
    }


    typedef void (Thing::*do_stuff_a)();
    typedef void (Thing::*do_stuff_b)(int);

    std::map<std::string, do_stuff_a> a_table;
    std::map<std::string, do_stuff_b> b_table;


    template<<MAP_MEMBER>,typename ... PP>
    void print_x(std::string s, PP &&... pp){
        auto handler = <MAP_MEMBER>[s];
        if(handler){
            (this->*handler)(std::forward<PP>(pp) ...);
        }
    }
public:
    
    typedef decltype(print_x<a_table>) print_a;
    typedef decltype(print_x<b_table>) print_b;

};

Any ideas on how to get rid of boilerplate is appreciated.

No need to get complicated, just use your printers as wrappers that pass a member to a generic print method like so:

class Foo
{
    int a;
    char b;
    
    template <typename M>
    void Print (M & member)
    {
        // complicated function 
    }
    
public:

    void PrintA ()
    {
        Print(a);   
    }

    void PrintB ()
    {
        Print(b);   
    }
};

So in your example the public print functions become wrapper functions:

class Thing
{
    // ...

    template <typename T, typename ... PP>
    void print (T & table, const std::string & key, PP && ... pp)
    {
        auto method = table[key];

        if (method)
            (this->*method)(std::forward<PP>(pp)...);
    }
    
public:

    template <typename ... PP>
    void print_a (PP && ... pp)
    {
        print(a_table, std::forward<PP>(pp)...);    
    }

    template <typename ... PP>
    void print_b (PP && ... pp)
    {
        print(b_table, std::forward<PP>(pp)...);    
    }
};

These public methods should be inlined if you use -O3 optimisation.

Here's a running solution with less boilerplate and no need for metaprogramming:

#include <iostream>
#include <string>
#include <map>

class Thing
{
    void do_a_1 ()
    {
        std::cout << "hello" << std::endl;
    }
    
    void do_a_2 ()
    {
        std::cout << "goodbye" << std::endl;
    }

    void do_b (int age)
    {
        std::cout << "my age is " << age << std::endl;
    }
    
    template <typename ... PP>
    using MapMethod = std::map<std::string, void (Thing::*)(PP...)>;

    MapMethod<> a_table;
    MapMethod<int> b_table;
    
    template <typename T>
    void insert (T) {}
    
    template <typename T, typename M, typename ... PP>
    void insert (T & table, const std::string & key, M && method, PP && ... pp)
    {
        table.insert({key, method});
        insert(table, pp...);
    }
    
    template <typename T, typename ... PP>
    void print (const T & table, const std::string & key, PP && ... pp)
    {
        auto result = table.find(key);
        
        if (result != table.end())
        {
            auto method = result->second;
        
            (this->*method)(pp...);
        }
    }
    
public:

    Thing ()
    {
        insert(a_table,
            "apple", &Thing::do_a_1,
            "banana", &Thing::do_a_2);
            
        insert(b_table,
            "ostrich", &Thing::do_b);
    }

    void print_a (const std::string & key)
    {
        print(a_table, key);
    }
    
    void print_b (const std::string & key, int val)
    {
        print(b_table, key, val);
    }
};

int main ()
{
    Thing t;
    
    t.print_a("apple");
    t.print_b("ostrich", 12);
    t.print_a("banana");
    t.print_a("Do nothing");
}

If somehow your wrapper methods are unavoidably repetitive in your real problem (perhaps perfect forwarding is getting tiresome), you could reduce boilerplate further with a macro to make the print methods:

class Thing
{
    // private code is the same, except:
    // there's no need for the generic print method anymore
    
public:

    Thing ();

#define PRINT_MACRO(FUNCTION_NAME, MEMBER_TABLE) \
    template <typename ... PP> \
    void FUNCTION_NAME (const std::string & key, PP && ... pp) \
    { \
        auto result = MEMBER_TABLE.find(key); \
        if (result != MEMBER_TABLE.end()) \
        { \
            auto method = result->second; \
            (this->*method)(pp...); \
        } \
    }

    PRINT_MACRO(print_a, a_table)
    PRINT_MACRO(print_b, b_table)
#undef PRINT_MACRO
};

Lastly, are you sure that you wanted to use std::map instead of std::unordered_map ? This problem suggests that you don't care about the ordering.

Macros or code generation are the way to go when you want to define things that require concatenating in identifiers.

In particular, since you also need to handle an arbitrary number of parameters in the generated code, I'd go with code generation.

You could extract the map to an extra class that can also handle searching and calling entries:


template <typename C, typename K, typename...Args>
class MemberFunctionCaller
{
private:
    using FunctionType = void(C::*)(Args...);
    using MapType = std::map<K, FunctionType>;

    MapType registry;


public:
    void Register(const K& key, FunctionType value)
    {
        registry[key] = value;   
    }

    void Call(const K& key, C* self, const Args&...args)
    {
        auto iter = registry.find(key);
        if(iter != registry.end())
        {
            FunctionType func = iter->second;
            (self->*func)(args...);
        }
    }
};

A simple typedef would simplify the usage inside your "Thing"-class:

    template <typename...Args>
    using ThingFunctionCaller = MemberFunctionCaller<Thing, std::string, Args...>;

The thing class could look somewhat like this:

class Thing{
    template <typename...Args>
    using ThingFunctionCaller = MemberFunctionCaller<Thing, std::string, Args...>;

private:

    void do_a(){
        std::cout << "hello" << std::endl;
    }
   
    void do_b(int age){
        std::cout << "my age is " << age << std::endl;
    }

    ThingFunctionCaller<> a_table;
    ThingFunctionCaller<int> b_table;

public:
    void print_a(std::string s){
        a_table.Call(s, this);
    }

    void print_b(std::string s, int val){
        b_table.Call(s, this, val);
    }
};

And that's what it would look like in Action: https://gcc.godbolt.org/z/KM5a85

You could use a static template registry that stores the relation between name and member function. This has other disadvantages but would work in your particular use case.

(With some additional coding you could even make this work more "natural").

template <typename C, typename...Args>
using MemberFunctionPointer = void(C::*)(Args...);

template <typename C, typename...Args>
class MemberFunctionCaller
{
private:
    using FunctionType = MemberFunctionPointer<C, Args...>;
    using MapType = std::map<std::string, FunctionType>;

    static MapType& GetRegistry(){
        static MapType registry;
        return registry;
    }

public:
    static void Register(const std::string& key, FunctionType function)
    {
        auto& registry = GetRegistry();
        registry[key] = function;   
    }

    static void Call(const std::string& key, C* self, const Args&...args)
    {
        auto& registry = GetRegistry();
        auto iter = registry.find(key);
        if(iter != registry.end())
        {
            FunctionType func = iter->second;
            (self->*func)(args...);
        }
    }
};

template <typename C>
class MemberFunctionRegistry
{
public:
    template <typename...Args>
    static void Register(const std::string& key, MemberFunctionPointer<C, Args...> function)
    {
        MemberFunctionCaller<C, Args...>::Register(key, function);
    }

    template <typename...Args>
    static void Call(const std::string& key, C* self, const Args&...args)
    {
        MemberFunctionCaller<C, Args...>::Call(key, self, args...);
    }

};

Your Thing class could look like this:


class Thing{

private:

    void do_a(){
        std::cout << "hello" << std::endl;
    }
    
    void do_a_extended(){
        std::cout << "hello world" << std::endl;
    }

    void do_b(int age){
        std::cout << "my age is " << age << std::endl;
    }

    MemberFunctionRegistry<Thing> registry;
public:
    
    static void RegisterMemberFunctions()
    {
        MemberFunctionRegistry<Thing>::Register("A", &Thing::do_a);
        MemberFunctionRegistry<Thing>::Register("AX", &Thing::do_a_extended);
        MemberFunctionRegistry<Thing>::Register("B", &Thing::do_b);
    }

    template <typename...Args>
    void print_x(std::string s, Args...args){
        registry.Call(s, this, args...);
    }
};

https://gcc.godbolt.org/z/fq5bez

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