Capture this in a lambda defined externally

Viewed 93

I have a class which has a std::function as a member variable.

class Animal
{
    public:
        Animal(const std::function<double(const int x)> MakeNoise) : MakeNoise(MakeNoise) {}
        void Print(const int x) { std::cout << this->MakeNoise(x) << std::endl; }
    private:
        const std::function<double(const int x)> MakeNoise;
        int a = 4;
        int b = 8;
        int c = 12;
};

I would like to be able to swap out the MakeNoise function without subclassing Animal by passing various lambdas.

const auto MakeNoise1 = [this](const int x)
{
    return a + b + x;
} 

const auto MakeNoise2 = [this](const int x)
{
    return a + b + c + x;
} 

Is it possible to capture this if the definition of the function is in a different file?

Is it possible to use [&] (capture by reference) to capture x pass into Print ?

Lastly, is there a better way to define this class so I can swap the function in and out?

If I add this the compiler says error: invalid use of ‘this’ at top level which makes sense since the definition of the lambda is not within a class.

3 Answers

I do not think this is possible to do directly. After all, the lambdas as given don't know anything about Animal. You can work around it by having the function's signature be (const Animal& animal, const int x) instead and accessing it through animal.

this is a special name inside member functions. If MakeNoise1 wants to capture this, it needs to be in a member of Animal. Your compiler told you so, and you interpreted that message corerctly.

That's not a big restriction, since Animal::a is private anyway.

You can define Animal methods in other .cpp files, but you'd still need to declare these methods in class Animal, so this might or might not match your larger design.

If you are defining the lambda outside a member function, then no, you cannot capture this because there is no this to capture. A capture is a way to provide access to variables that are defined at the point where the lambda is defined. A capture cannot capture things that do not exist at the point of capturing.

What you want to do is provide access to variables that are defined at the point where the lambda is invoked. This is the job of parameters, as in this->MakeNoise(this, x) or perhaps MakeNoise(*this, x). (Your Print wrapper can easily provide the extra parameter. In fact, adding parameters is a common motivation for writing wrapper functions.) However, I suspect that this might not be the best approach.


Instead of thinking about how to access this, think about what MakeNoise is supposed to do and what it needs to do that. Does it really need the entire Animal including private data? If so, it probably should be a member function. Bite the bullet and create a plethora of derived classes (and provide protected access to the data). Does it instead need the entire Animal, but only the public interface? If so, a lambda that takes both const Animal & and const int as parameters might be reasonable. Furthermore, it might be reasonable to expand the public interface to accommodate this.

Perhaps, though, you are in the case where MakeNoise does not really need an Animal so much as a few key bits of data. This is the point where you have to look at your design and your levels of abstraction. We cannot do this for you because, as is appropriate for a StackOverflow question, we do not have the complete picture. However, I can present for consideration the possibility that things other than animals can make a noise. Are your MakeNoise lambdas supposed to be abstract enough to not care what is making the noise? If so, you might consider adding specific data as parameters to your lambdas. Your Print function would become something more like the following.

    void Print(const int x) { std::cout << MakeNoise(x, a, b, c) << std::endl; }

I am assuming that Animal has been (appropriately) simplified for this question, and that an Animal object really has a lot more data than a, b, and c. If this assumption is false, you are in the case of needing an entire Animal. However, if the parameters you would need to pass to MakeNoise are few in comparison to the data in Animal, this might be a better semantic fit to your design. Might. It all comes back to making design choices that are sensible and consistent. Think abstractly while avoiding over-engineering. Keep in mind that you need to provide the same parameters to each lambda (but the various lambdas can have different captures).

Here is an example lambda that could be used for this last approach, assuming the type of MakeNoise – both of the data member and of the parameter to the constructor – has been updated.

int main()
{
    Animal cheetah{ [](int x, int a, int b, int c) -> double
                    {
                        return a + b + c + x;
                    }
                  };
    cheetah.Print(2);
}

If you really want to use const int instead of int, you could. To me, it seems unnecessarily restrictive for a non-reference, but that's more style than substance.

Related