How to correctly copy a lambda with a reference capture?
Asked Answered
P

3

0

Ok so I ran into a problem when implementing a c# property like system in c++ (see: https://mcmap.net/q/16573/-c-like-properties-in-native-c).

Consider the following example:

struct TransformCmp
{
    PropertyDelGetSet<vec3> position =
        PropertyDelGetSet<vec3>(
            [&]() -> const vec3& { return m_position; },
            [&](const vec3& val) { m_position = val; m_dirty = true; });

private:
    bool m_dirty = true;
    vec3 m_position = vec3(0);
}

If/when the instance of the TransformCmp is copied/moved (e.g. if it was stored in std::vector and resize was called), the reference capture is now invalid.

The question is how do I make sure when the copy/move happens that I also update the reference capture?

I've tried implementing a copy constructor for the Property classes but I ran into the same issue, probably because I didn't do it correctly. Any ideas?

Update:

I'm trying to do a similar idea to the functors Matthias Grün suggested, basically I'm passing a TransformCmp pointer to the PropertyDelGetSet constructor, which will be passed on the get-set functions.

When initializing the property I'm doing something like this:

PropertyDelGetSet<TransformCmp, vec3> position =
    PropertyDelGetSet<TransformCmp, vec3>(this, // <- now passing this
        [](TransformCmp* p) -> const vec3& { return p->m_position; },
        [](TransformCmp* p, const vec3& val) { p->m_position = val; p->m_dirty = false; });

However, I need to be able to update the pointer stored in PropertyDelGetSet to make this work.

Phagy answered 28/7, 2021 at 11:46 Comment(2)
Since you don't have direct access to the data which implements the reference capture, you cannot "update" the reference capture. Hence you would have to recreate the lambda. Or you don't use lambdas (but implement the closure type yourself). More generally, you have created a self-referencing type.Alloy
Can I recreate the lambda with a copy constructor?Phagy
P
0

I think I've found the best solution.

The idea is to call the constructor from the copy constructor and then manually set the rest of the members which can be copied trivially:

struct TransformCmp
{
    TransformCmp() {}

    TransformCmp(const TransformCmp& other)
        : TransformCmp() // Makes sure the lambda refs are updated
    {
        // Trival copy of members
        m_dirty = other.m_dirty;
        m_position = other.m_position;
    }

    PropertyDelGetSet<vec3> position =
        PropertyDelGetSet<vec3>(
            [&]() -> const vec3& { return m_position; },
            [&](const vec3& val) { m_position = val; m_dirty = false; });

private:
    bool m_dirty = true;
    vec3 m_position = vec3(0);
};

This way there's no need to pass around the TransformCmp pointer around in the Property classes which is a lot cleaner. If there was a way to also call the generated copy constructor after overriding it, it would be even cleaner, but this is quite satisfactory to me.

Phagy answered 28/7, 2021 at 15:44 Comment(1)
I'm accepting this as the answer, but if this can be improved please let me know!Phagy
E
1

You could create functors instead of using lambdas, somewhere along the lines of this:

struct Getter {

    const vec3& operator()() const noexcept { return m_pRef->m_position; }

    TransformCmp* m_pRef{};
};

struct Setter {

    void operator()(const vec3& pos) noexcept { m_pRef->m_position = pos; }

    TransformCmp* m_pRef{};
};

Then pass instances of these to PropertyDelGetSet. During copies and moves, you could then update the m_pRef pointer to point to the correct instance, sort of like this:

struct TransformCmp
{
   ...
   TransformCmp(const TransformCmp& other) : position{ other.position }
       position.getter().m_pRef = this;
   }
   ...
}

assuming that PropertyDelGetSet::getter() will return a Getter& through which the contained functor can be retrieved.

Lambda captures cannot be accessed from outside, since they are private to the lambda.

Expire answered 28/7, 2021 at 12:30 Comment(8)
I will have a look at this, but I would prefer to use lambdasPhagy
How would I go about updating the m_pRef pointer? I've updated my question as I think I can do something similar whilst keeping the lambdasPhagy
Of course your solution of always passing the right instance to the lambda is also a viable solution!Herodias
Got it to sort of work haha looks really ugly tho as I have to re-construct the PropertyDelGetSet instance in the copy constructor which is rather verbose and not clean. However, now that it's working I'm going to try to clean it up! Thanks a lot for the help :)Phagy
Please consider marking the answer as correct if it helped :-)Herodias
It certainly helped, however, it doesn't answer the original question I'm afraid. If you can come up with a solution that uses lambdas (currently trying to make a clean version) I'd be more than happy to mark as correct!Phagy
No worries :-) Using lambas, you will have to pass the instance, i'm afraid, like you suggested yourself.Herodias
Indeed, basically, now it comes down to finding a way to automatically get the right pointer. I'm thinking of maybe finding a way to wrap 'this' with a class and using its default copy constructor to get the right pointer. If that works then I can pass that to the Property in the constructor. Although not sure if this is the right way atm.Phagy
B
0

Here's how we do this back on my home planet.

struct TransformCmp
{
  void setPosition(const vec3& val) { m_position = val; m_dirty = true; }
  vec3 getPosition() { return m_position; }
private:
  bool m_dirty = true;
  vec3 m_position = vec3(0);
};

See? No lambdas, no captures, no need to update anything, no overhead of type erasure, nothing.

But! but! but! what about my design? I have a design! with properties! and templates! and stuff!

Tell you what. This design is no good.

There is nothing inherently wrong with having complex thingies ("properties") that internally manage references to your object. However, if you want to store these thingies within the object itself, this ain't gonna fly through code review. The object has a position and a dirty flag. They can be manipulated through an absolutely minimal interface of two member functions, and absolutely minimal it shall remain.

If a property-like object is needed, for example for unification with other similar interfaces, then create one on the fly, use it, and drop it before the object has a chance to move. It has no business being stored as a part of the object. Separation of concerns.

Bozo answered 28/7, 2021 at 15:20 Comment(2)
The reason why I'm using the property class is that it's tied with a scripting system that automatically binds the member to the scripting language, and it knows how to bind property classes and whether it's get-set or get only.Phagy
P.S. On my planet we've already found a solution :)Phagy
P
0

I think I've found the best solution.

The idea is to call the constructor from the copy constructor and then manually set the rest of the members which can be copied trivially:

struct TransformCmp
{
    TransformCmp() {}

    TransformCmp(const TransformCmp& other)
        : TransformCmp() // Makes sure the lambda refs are updated
    {
        // Trival copy of members
        m_dirty = other.m_dirty;
        m_position = other.m_position;
    }

    PropertyDelGetSet<vec3> position =
        PropertyDelGetSet<vec3>(
            [&]() -> const vec3& { return m_position; },
            [&](const vec3& val) { m_position = val; m_dirty = false; });

private:
    bool m_dirty = true;
    vec3 m_position = vec3(0);
};

This way there's no need to pass around the TransformCmp pointer around in the Property classes which is a lot cleaner. If there was a way to also call the generated copy constructor after overriding it, it would be even cleaner, but this is quite satisfactory to me.

Phagy answered 28/7, 2021 at 15:44 Comment(1)
I'm accepting this as the answer, but if this can be improved please let me know!Phagy

© 2022 - 2024 — McMap. All rights reserved.