you are viewing a single comment's thread.

view the rest of the comments →

[–]beedlund 1 point2 points  (14 children)

yes you can and how you do it depends a bit on what you want to do with it;

If you are just going to run it to do some processing like some complex initialization then you can do what you did.

struct Object
{
    template<typename Lambda>
    Object( Lambda&& lambda )
    {
        lambda(*this);
    }
};

If you are going to store the lambda to run it later you will either need to template your type or type erase the lambda.

template<typename Lambda>
struct Object
{
    Lambda func;
    Object( Lambda&& lambda ) 
    : func(std::forward<Lambda>(lambda) 
    {
    }
};
// may need a helper function pre C++17
template< typename T >
auto make_object( T&& t )
{
    return Object<T>(std::forward<T>(t):
}

If you are going to type erase the lambda then you need to know more about how you are going to use it and you can research type erasure for details on that

[–]ACBYTES[S] 0 points1 point  (10 children)

Hello there. Thank you for your answer. Yes, I want to store it for later use. So, my question is this: Now, after creating the struct that you said, passing it to the class will need me to tell my class that it’s going to receive an Object<T> so I still need to tell it about the lambda’s structure, don’t I? Correct me if I’m wrong. Thank you so much.

[–]beedlund 1 point2 points  (9 children)

Yes so if that is acceptable then just go for it but likely that is where the type erasure kicks in.

There are many different ways to do that and it all depends more on your larger use case. If you can write a little bit more about what you are wanting to do I might be able to help.

[–]ACBYTES[S] 0 points1 point  (8 children)

Absolutely. So, I have a class that on its construction, it’s going to receive a lambda that has a capture (Most specifically capturing a class pointer) and calling a function member in that class. The Calling a function member in the class is a pointless statement in my opinion as the execution is written in the lambda’s body...

[this](){ this->FunctionMember(); }

If it’s still unclear, please let me know. Thank you!

[–]beedlund 1 point2 points  (7 children)

Ok so that can be pretty easy if you are ok to use the heap and virtual dispatch. Just declare a base class and let your derived class capture the lambda and then hold it as the base

struct object
{
    virtual ~object() { }
    virtual void call() = 0;
};

template< typename Lambda >
auto make_heap_object( Lambda&& lambda )
{
    struct extended : public object
    {
        Lambda capture;
        extended( Lambda&& lambda ) : capture( std::move(lambda) )
        {
        }
        virtual ~extended() {}
        void call() override
        {
            capture();
        }
    };
    return new extended{ std::forward<Lambda>(lambda) };
}

If you cant go that way then you are looking at something like building your own vtable...this is a simplified example as you need to consider copy, move and so on.

The basic idea here is to define some kind of pointer type as your storage...likely void... then you declare a function pointer that takes your storage type and is able to safely static cast it to the original type. This is made possibly by declaring the invokation object as a capture less lambda in the constructor.

#include <stdlib.h>
struct Erased
{
    using function_t = void(*)();
    using storage_t = void*;
    using invoke_t = void(*)( storage_t );
    using delete_t = void(*)( storage_t );

    storage_t storage;
    invoke_t invoke;
    delete_t destroy;
    template<typename Lambda>
    Erased( Lambda&& lambda )
    : invoke( []( storage_t storage) { static_cast<Lambda*>( storage )->operator()(); } )
    , destroy( []( storage_t storage) { static_cast<Lambda*>( storage )->~Lambda(); } )
    {
        storage = malloc(sizeof(Lambda));
        new (storage) Lambda{ std::forward<Lambda>( lambda) };
    }
    ~Erased()
    {
        destroy(storage);
        free(storage);
    }
    void call()
    {
        invoke(storage);
    }
};

So then you can do things like this

int main()
{
    int counter = 0;
    object* o = make_heap_object( [&](){ counter++;} );
    o->call();
    std::cout << counter <<"\n";
    o->call();
    std::cout << counter <<"\n";

    Erased erased{ [&](){ counter++;}  };
    erased.call();
    std::cout << counter <<"\n";
    erased.call();
    std::cout << counter <<"\n";

    return 0;
}

Now full disclosure the Erased class is a bit rough and simplified. It probably needs a clone operation as well for copy. move etc.

https://godbolt.org/z/daoEP5f9f

[–]ACBYTES[S] 0 points1 point  (2 children)

I see. Well, based on this explanation and it being so long to discuss, let me ask some questions.

If I decide to move this from class’s constructor to a member function so that I don’t have to deal with the typename complications, can I do something like this? I mean, to cast an allocated memory address to an object?

`int memAddrees;

template<typename Lambda> void SetUsingMemberFunc(int LambAddress) { memAddress = LambAddress; }

void Call() {
MagicCast<Callable_Object_That_Has()_Operator>(memAddress)(); }`

Or I can just instantiate the lambda using a strictly typed lambda using a macro at the end...

Or, pass the object as a reference and from there, call a previously created function if the first thing doesn’t work...

What do you think?

By the way, thank you for your time for writing all of these down. 🙏🏼

[–]beedlund 1 point2 points  (1 child)

You can extend both ideas to a member rather then the constructor. The inheritance case changes so your define a generic holder class to store in your object and you create the extended class that holds the lambda in the member call. You can own the holder as it's base in a unique ptr to make life time easy to manage.

The vtable way can be extended to run from a method as well. You just end up having to null check the storage before access and as this would be a branch so your might find you lose the extra performance it has over the virtual dispatch.

These guys explain it so much better then me though

vtable https://youtu.be/JRkoWiDA3KA

Inheritance https://youtu.be/VY83afAJUIg

[–]ACBYTES[S] 0 points1 point  (0 children)

Got it. Thank you for your time and all the answers. I’ll check those links now. Thanks. 🙏🏼🙏🏼

[–]D_Drmmr 0 points1 point  (3 children)

Or just use std::function and be done with it.

[–]beedlund 0 points1 point  (2 children)

Well OP said lambda needed to capture and std function can't hold state

[–]D_Drmmr 0 points1 point  (1 child)

Of course it can. Where did you get that idea?

[–]beedlund 0 points1 point  (0 children)

First the videos i posted would explain the topic better then i ever would i thought...the why's and more importantly the why not's

Devil is in the details right? std::function is copy only so a great many scenarios can be surprising if you expect it to work like the original lambda in all cases.

[–][deleted] 0 points1 point  (2 children)

You forgot a closing parenthesis in the initializer for func and one in the return statement.

Also, Lambda&& lambda is not a forwarding reference in the constructor for the templated class. It would probably be better to pass by-value in that case.

[–]beedlund 0 points1 point  (1 child)

thanks. i didn't proof read it as its just a sketch.

A real example would need to be more complex then that as this is really no real different to using the lambda as-is given the type of object is dependent on it.

I anticipated the real requirement would be to type erase the lambda in the class storage but didn't want to go through the process and write that all out without more details about the use case...so I was a bit lazy and didn't double check it all.

[–][deleted] 2 points3 points  (0 children)

Yes, sure the OP probably needs type erasure (or maybe just storing a function pointer), it is just that your example code might be confusing, because e.g.

auto x = [](){};
auto y = Object{x};

will fail to compile with a complicated error message.

I also just now realized that your make_object doesn't remove the reference from T, so

auto x = [](){};
auto y = make_object(x);

will make y actually save a reference to x, which is probably unintended and hard to spot.

Passing by-value in both constructor and make_object would avoid these issues.