Belfer4
Belfer4

Reputation: 361

How to correctly copy a lambda with a reference capture?

Ok so I ran into a problem when implementing a c# property like system in c++ (see: https://stackoverflow.com/a/68557896/3339838).

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.

Upvotes: 0

Views: 698

Answers (3)

Belfer4
Belfer4

Reputation: 361

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.

Upvotes: 0

n. m. could be an AI
n. m. could be an AI

Reputation: 120239

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.

Upvotes: 0

Matthias Gr&#252;n
Matthias Gr&#252;n

Reputation: 1506

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.

Upvotes: 1

Related Questions