C++ save lambda-functions as member-variables without function pointers for optimization
Asked Answered
M

4

7

I would like to write in C++ a class that saves lambda functions as member variables. It would be great to do it as efficient as possible. For example, I read this thread Why can lambdas be better optimized by the compiler than plain functions? and therefore I want to avoid using function pointers.

So far my best solution is the following:

template<typename F>
class LambdaClass {
  private:
    F lambdaFunc;
  public:
    LambdaClass(F &_lambdaFunc): lambdaFunc(_lambdaFunc) {}
};

I would use this class as follows:

auto lambdaFunc = [](int _a) -> int { return _a; };
LambdaClass<decltype(lambdaFunc)> lambdaClassObject<decltype(lambdaFunc)>(lambdaFunc);

In my opinion this doesn't look like fun using it. So I am interested in first if this code is efficient in the sense that the compiler could inline the calls of the saved member lambda function and second how one could write this code more beautiful?

Edit: I am using C++ 11.

Maidservant answered 14/8, 2018 at 9:6 Comment(1)
Note that until 2020 you'll have to use something like std::function to make it assignable.Bantamweight
C
10

In your example

LambdaClass<decltype(lambdaFunc)> lambdaClassObject<decltype(lambdaFunc)>(lambdaFunc);

the second template argument list is incorrect syntax. This needs to be just

LambdaClass<decltype(lambdaFunc)> lambdaClassObject(lambdaFunc);

So I am interested in first if this code is efficient in the sense that the compiler could inline the calls of the saved member lambda function

Yes, this class can be used in ways that will allow optimizations pretty much just like using a lambda directly. The template argument is the exact type of the lambda expression, and template substitution happens at compile time, usually giving results just like you would get by writing out code without using templates.

How one could write this code more beautiful?

@lubgr's answer already mentions the C++17 "class template deduction" and "deduction guide" features. Prior to C++17, the usual trick to avoid needing to specify class template arguments is a helper "make function":

template <typename F>
auto makeLambdaClass(F&& func) ->
    LambdaClass<typename std::decay<F>::type>
{ return { std::forward<F>(func); } }

Now you can do

auto lambdaFunc = [](int _a) -> int { return _a; };
auto lambdaClassObject = makeLambdaClass(lambdaFunc);

But to go a step further and make

auto lambdaClassObject = makeLambdaClass( [](int _a) -> int { return _a; } );

also work, you'll also need to make sure the class has a constructor that accepts an rvalue, not just a non-const lvalue:

template<typename F>
class LambdaClass {
  private:
    F lambdaFunc;
  public:
    LambdaClass(const F &lambdaFunc_): lambdaFunc(lambdaFunc_) {}
    LambdaClass(F &&lambdaFunc_) : lambdaFunc(std::move(lambdaFunc_)) {}
};

By the way, this class will work just as well with a callable class that is not a lambda's closure type, since a lambda is just a more convenient way of defining a class with an operator():

class UniqueUIntGenerator
{
public:
    unsigned int operator()() const noexcept
    { return num++; }
private:
    static unsigned int num;
};
unsigned int UniqueIntGenerator::num = 0;

LambdaClass<UniqueIntGenerator> gen{UniqueIntGenerator{}};
Cristiecristin answered 14/8, 2018 at 10:4 Comment(0)
R
5

When you're able to use C++17, you can use template argument deduction for classes. The instantiation then looks like this:

auto lambdaFunc = [](int _a) -> int { return _a; };
LambdaClass lambdaClassObject(lambdaFunc);

which looks like more fun using it. This use case does not impose any restrictions when it comes to inlining the lamdba invocation.

Note that it might be desirable to pass temporaries to you constructor. In this case, use an rvalue reference that is explicitly turned into a forwarding reference by the necessary deduction guide:

template<typename F> class LambdaClass {
    private:
        F lambdaFunc;
    public:
        LambdaClass(F&& _lambdaFunc) : lambdaFunc(std::forward<F>(_lambdaFunc)) {}
};

// Deduction guide, makes the ctor accept lvalue and rvalue arguments:
template<class F> LambdaClass(F&&) -> LambdaClass<F>;

You can now instantiate a LambdaClass object with the lvalue-lamdba above or by

LambdaClass lambdaClassObject([](){ /* Do stuff. */ });

As pointed out by @Holt, @aschepler and @Caleth in the comments, the type deduction results in F& when lvalues are passed to the constructor, which is unlikely to be the desired instantiation. I couldn't get std::remove_reference_t or std::decay_t to do the work in the deduction guide as suggested by @Caleth, but found a solution that requires no deduction guide, but instead an overloaded constructor:

template<typename F> class LambdaClass {
    private:
        F lambdaFunc;
    public:
        LambdaClass(F&& _lambdaFunc) : lambdaFunc(std::forward<F>(_lambdaFunc)) {}
        LambdaClass(F& _lambdaFunc) : lambdaFunc(_lambdaFunc) {}
};

// Note: Deduction guide is gone. The first ctor accepts only an rvalue reference.

This allows construction with lvalue or rvalue arguments with the "intuitive" type being deduced by the compiler.

Rammer answered 14/8, 2018 at 9:14 Comment(13)
Thanks for your answer! Unfortunately I am using C++ 11.Maidservant
Ok, too bad. I'll keep the answer for possible future reference.Rammer
@Rammer "LambdaClass(F&& _lambdaFunc)" is not a forwarding reference, or am i wrong? after going through the deduction guide, the template parameter is fixed and so it is a rvalue referenceAcuminate
@phön I was thinking about the correct terminology there, too. I'll read through it once more and fix it. Thanks for the hint.Rammer
@Rammer But i seems to work with lvalues too. i guess thats because reference collapsing? The deduction guide leads to F == lvalue and so we have F& &&. I am not sure thoAcuminate
@phön The deduction guide turns F&& into a forwarding reference. Only without the deduction guide, it's an rvalue reference. See here (at the bottom).Rammer
@Rammer In the constructor argument lists, F&& is always an rvalue-reference to F. With the deduction guide, you allow F (the class template argument) to be T or T& (where T is the type of the lambda), depending on the argument of the constructor. Then you get reference collapsing in the constructor, but in one case lambdaFunc is a reference, in the other case it's not, that can be very misleading.Romina
@Rammer Under en.cppreference.com/w/cpp/language/… in the 4th example from the bottom it is shown, that in this case F would be no forwarding reference but an rvalue ref with reference collapsing. its after the paragraph "An rvalue reference to a cv-unqualified template parameter is not a forwarding reference if that parameter is a class template parameter:"Acuminate
Yes, that deduction guide allows the class template parameter to be an lvalue reference, which might lead to unfortunate surprises. For example, SomeSortOfTypeErasure wrapper; { int n = 0; auto gen1 = [&n](){ return n++; }; LambdaClass gen2(gen1); wrapper = gen1; }Cristiecristin
@phön a forwarding reference is "an rvalue ref with reference collapsing". You just get LambdaClass<Sometype &> when you Sometype some; LambdaClass lambda(some);, instead of LambdaClass<Sometype> when you LambdaClass lambda(Sometype{});Shaniceshanie
@Rammer you may want to add std::remove_reference_t or std::decay_t to the deduction guideShaniceshanie
@Shaniceshanie Thanks, I was thinking about a possible mitigation. Seems the way to go.Rammer
@Shaniceshanie The result is the same. But technically there are 2 steps involved from my understanding (I may be wrong). First deduce the template parameter from the guide. Then do template substitution. This will lead to reference collapsing and the same net result for the user, but technically the constructor argument is no longer a forwarding reference.Acuminate
B
3

In C++ 11, only functions can deduce template arguments automatically. So, just write a make-function:

template<typename F>
LambdaClass<F> makeLambdaClass(F&& f)
{
  return LambdaClass<F>(std::forward<F>(f));
}

This lets you omit the template argument in the usage:

auto lambdaFunc = [](int _a) -> int { return _a; };
auto lambdaClassObject = makeLambdaClass(lambdaFunc);

You should however be aware that this only passes the problem upwards: If somebody else wants to use LambdaClass as a member, they either have do the same class templating shenanigans or roll their own type erasure. Maybe this is not a problem for you, but you should be aware of it.

Bobbee answered 14/8, 2018 at 9:37 Comment(0)
B
2

In C++11, the simplest one can achieve is to delegue type inference to a template-function:

template<class F>
auto make_lambda_class(F&& f) -> LambdaClass<decltype(f)>
{ return f; }

You would call it like:

auto lambdaFunc = [](int _a) -> int { return _a; };
auto lambdaClassObject = make_lambda_class(lambdaFunc);

But, sadly, you cannot pass an rvalue to make_lambda_class. You'd need C++17 to do that, as lubgr shown.

Blackmon answered 14/8, 2018 at 9:37 Comment(7)
I find relying on the implicit conversion there a bit unexpected/bold. True, the LambdaClass constructor should probably be explicit, but I wouldn't assume it as intended.Bobbee
Well, if somebody decides that the LambdaClass constructor should be explicit (for example, because their Lint tool yells at them), your make_lambda_class would no longer work. This would not happen if you were to explicitly call the constructor.Bobbee
@MaxLanghof Yes. When you change a class constructor, you should be ready to change the way it's called. The Open/Close principle of SOLID advises that a software entity should be open for extension, but closed for modification. Making a constructor explicit is clearly a modification.Blackmon
I don't see how any of this justifies relying on implicit conversion when you know you want to call the constructor. I mean, it's not a big deal, I'm just curious if you see any advantage.Bobbee
The only reason this doesn't allow an rvalue argument is that the class shown doesn't have a constructor that accepts a non-const rvalue. If the class allows it, you can use an rvalue just fine in C++11. (Just change the make_lambda_class body to { return std::forward<F>(f); }.) Note that decltype(f) can be an lvalue reference type, so LambdaClass<decltype(f)> will have a reference member instead of a class member, which could possibly cause unpleasant surprises. This is why I recommend LambdaClass<typename std::decay<F>::type> in my answer.Cristiecristin
@Blackmon Sorry but that's a terrible reason. By that logic, we should all be using golfing languages or only use single letter variable names. Because clearly that improves readability.Bobbee
I am not annoyed (and I hope I made clear that I didn't consider this topic real flaw in your answer), and I'm surprised you think I would have those motivations (accusing of plagiarism on this kind of trivial answer? Really?). I was legitimately trying to learn because there might have been some deeper meaning I was unaware of or some other functional difference - it wouldn't be the first time. But as suggested, I'll drop the topic and continue on my way.Bobbee

© 2022 - 2024 — McMap. All rights reserved.