Search code examples
c++hashfunctional-programmingc++17template-meta-programming

Unique id for any kind of callable object in C++17


In one part of my code, I have an abstract function type Function which represents any kind of callable and which can be stored in a heterogeneous container, e.g. std::vector<std::unique_ptr<Function>>:

#include <any>
#include <string>
#include <memory>
#include <vector>
#include <functional>
#include <cassert>

class Function
{
public:
    Function(std::string name)
     : m_name(name)
    {}

    virtual ~Function(){}

    std::string name() {
        return m_name;
    }
    
    template <typename... Args>
    decltype(auto) operator()(Args&&... args)
    {
         // delegate to invoke, implementation not relevant for question
    }
private:
    std::string m_name;
    // the following is also simplified for the sake of brevity
    virtual std::any invoke(std::initializer_list<std::any> const& args) const = 0;
};

template <typename F>
class FunctionImpl : public Function
{
public:
    FunctionImpl(F const& f, std::string name)
     : Function(name)
     , function(f)
    {}

private:
    std::any invoke(std::initializer_list<std::any> const& args) const override
    {
        // implementation not relevant for question
        return std::any();
    }

    F function;
};

using FPointer = std::unique_ptr<Function>;

template <typename F>
FPointer make_function(F const& f, std::string name)
{
    return std::make_unique<FunctionImpl<F>>(f, name);
}

Now I want to add a function

using FContainer = std::vector<FPointer>;

template <typename F>
bool contains(FContainer const& vec, F const& f)
{
    // ?????
}

which returns true, if the function passed as argument in contained in the container, and false otherwise (and probably in a follow-up step a function that returns a reference to the element in the container, if it is contained). How would I write this kind of function? What are my options?

void bar(){}
void foo(){}

struct AClass {
    void MemberFunction1(){}
    void MemberFunction2(){}
};

struct ACallableClass
{
    void operator()(){}
};

int main()
{
    FContainer v;

    // function pointer
    v.push_back( 
        make_function(
            &foo,
            "foo"
        )
    );

    // std::function
    v.push_back( 
        make_function(
            std::function<void()>(&foo),
            "foo"
        )
    );

    // member function
    v.push_back( 
        make_function(
            &AClass::MemberFunction1,
            "AClass::MemberFunction1"
        )
    );

    // callable
    v.push_back(
        make_function(
            ACallableClass(),
            "CallableClass"
        )
    );

    // lambda
    v.push_back( 
        make_function(
            [](){},
            "empty lambda"
        )
    );

    assert(contains(v, &foo));
    assert(contains(v, std::function<void()>(&foo)));
    assert(contains(v, &AClass::MemberFunction1));
    assert(!contains(v, [](){})); // every lambda is different

    assert(!contains(v, &bar));
    assert(!contains(v, std::function<void()>(&bar)));
    assert(!contains(v, &AClass::MemberFunction2));

    return 0;
}

The best solution I could come up with so far was to write a function template

template <typename F> size_t id(F&& id);

that gives a unique id to any kind of callable. Then Function could get a new virtual size_t id() const = 0 method, which would be overwritten by Function<F>. The latter delegates to the free function template. With this, I could compare ids in contains.

I tried implementing the function template using std::hash with function pointers, but I got stuck at hashing member function pointers, callable classes and lambdas. Here is my latest approach: https://godbolt.org/z/zx4jnYbeG.

Sorry for the rather lengthy question. Any help would be greatly appreciated!

EDIT 1:

I can live without std::function support. I would like to support lambdas in principle, but I can live with contains always returning false for lambdas, which makes sense to me. I do want the code to work with function pointers, callable classes and member functions.

EDIT 2:

Here is a working solution based on the suggestions in xryl669s answer: https://godbolt.org/z/vYGesEsKa. std::function<F> and F get the same id, but I suppose this actually make sense, since they are basically equivalent.


Solution

  • Use an unordered_map and not a vector to store your functions. The key can be derived from the name (probably better anyway), or from the address of the function, but in that case, you'll have an issue with everything that's not a real function (like a method, a std::function<> instance, a lambda, ...)

    But since you probably have an issue already with your make_function for methods (you don't capture the instance), you can probably make a specialization for that case using a lambda or a template trampoline and use that address instead.

    Another issue to account for is:

    1. std::function<>(&foo) != std::function<>(&foo) (you have 2 instances, they are 2 different objects)
    2. Similarly for lambda functions, two different instance containing the same lambda body won't match anyway.
    3. Compiler is allowed to generate copies of functions if it has all the code for them and it's doing so unless you build with -Os or use external linkage for your functions

    So, unless you fallback to a unique identifier that you assign to your Function, you can't assert that a function is identical to another based on the function's body or some instance.

    Example (working) godbolt for the specialized template approach: https://godbolt.org/z/8sP5MfG6r

    Please notice that you can't store a &foo and std::function<>(&foo) in the container in this approach if using the std::function<>::target() as the key, they'll point to the same function and thus will be overwritten or not inserted since they already exist, but that's probably a good thing for your application it seems.

    If you don't care about UB, you can use this version: https://godbolt.org/z/9GoEWMnMb that's reinterpret_cast'ing the function's pointer (and pointer to method too) to use as the hash's key in the map. That's not clean, but since we don't use the result of the cast to call the function, it shouldn't bother much.