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

1

0

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.

Whitt answered 16/9, 2022 at 11:36 Comment(15)
Is there a reason you can't just use typeid? std::type_info is comparable and hashable already.Catholicism
@Catholicism Thought of something alike as well – though that relies on the type only and wouldn't distinguish different instances of the same type. Might or might not be an issue – QA: please clarify...Fossil
Not sure why you would need this at all. What are you intending to solve with? Possibly an XY problem...Fossil
Taking the pointer to a member function without capturing the instance is very strange, not to say the less. How can one know if it need an instance of a specific class when invoking ?Cruel
Hashing alone is usually not sufficient, unless you invent a way to crate a perfect hash for your case, because of possible collisions. The proper way would be delegate job to the operator== of stored callable.Numerary
@Cruel OP mentions that he uses invoke under the hood. With invoke you can threat member functions as normal functions with reference to object as additional first argument obj.*mptr(args...)invoke(mptr, obj, args...)Numerary
Wouldn't typeid return the same id for two different functions with the same signature? godbolt.org/z/xMbqKcfKjWhitt
Lie Revolver_Ocelot said, with std::invoke I don't have to capture the instance for the member function.Whitt
@Aconcagua, might be an XY-Problem, I am not sure. My code basically models a minimalistic dynamically typed sublanguage in C++ using a reflection system. Reflected types and functions are stored in heterogeneous containers. Now given a new function or an instance of any type, I would like to find out if it has previously been registered in the reflection system by searching the corresponding container.Whitt
Are you registering types or objects?Muscarine
types, or rather, objects describing the types. But I don't have an issue with the type registry, I have an issue with the function registry, because it doesn't suffice to describe the type of the function, because two distinct functions with the same signature could have the same type. So I need to deal with types and functions differently, hence the approach from the question.Whitt
Hm. There is a contradiction right here. If you are registering types, why do you care about different values having the same type? It looks like you want to register values, and that's a problem, because in general the only unique identifier of the value is the value itselfMuscarine
Ah sorry, I think there is a misunderstanding: I am distinguishing "registering types" and "registering functions" in the reflection system. So the reflection system has two seperate subsystems, one for types and one for funtions - precisely because for the first, I don't care about different values having the same entry and for the latter I do care. This question concerns only the function registry, I just wanted to add more context with my previous comment.Whitt
For the types you can just use type_info as the key. For the functions it's a problem. You can't in general get a unique ID of an arbitrary function object (or any other arbitrary object). But why do you need that? Can you compare arbitrary function values in your language? That would be a very unusual language feature.Muscarine
Let us continue this discussion in chat.Whitt
C
1

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.

Cruel answered 16/9, 2022 at 12:5 Comment(10)
Thanks for the reply. I don't believe I have an issue with methods if I can work with std::invoke. I can't use the name as key, because the whole point of the contains function is that I pass any type of callable and not an instance of Function. So I am stuck with the problem of pointer to methods (among others). Finally, what do you mean by a template trampoline?Whitt
Note that you can cast pointers to member functions to pointers to another type – though using the cast pointer to call the pointee would yield UB, meaning that you need to cast back before doing so.Fossil
Can you though? isocpp.org/wiki/faq/…. If I could, I wouldn't mind the UB because I don't intend to call the pointee but just use the pointer to generate a unique id. EDIT: I just tested it on godbolt, you can't static_cast, but you can implicit cast and get a warning from gcc. I am not super comfortable with it...Whitt
What I mean by a trampoline is that you make a specialization for FunctionImpl<std::function<T>> and capture the target() for deducing the id not the instance of the std::function itself.Cruel
Ah ok, gotcha. Though your godbolt example still fails on the first std::function assert, right? BTW, I can live without std::functions. Function pointers, member function pointers and callable classes would be cool.Whitt
Sorry, I copied the wrong link. I've edited the answer with the right link, every assert passes.Cruel
Actually, it segfaults before the first assert (-fsanitize=adress)Whitt
Ok, fixed the issue, and it's working with all your cases including std::function. I had to store the address of the functions in variable because else the compiler is generating different values each time. It shouldn't happen if the function had external linkage.Cruel
Ah nice! We are getting there, thanks a lot! Unfortunately, storing the addresses of the functions is not an option for me. The problem seems to be that you form pointers to (member) function pointers. I suppose this can be caught with if constexpr, and std::is_pointer and std::is_member_function_pointer... I will experiment a bit laterWhitt
Let us continue this discussion in chat.Cruel

© 2022 - 2024 — McMap. All rights reserved.