6

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.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
cowculus
  • 63
  • 5
  • 1
    Note that until 2020 you'll have to use something like `std::function` to make it assignable. – bipll Aug 14 '18 at 10:18

4 Answers4

9

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{}};
aschepler
  • 70,891
  • 9
  • 107
  • 161
4

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.

lubgr
  • 37,368
  • 3
  • 66
  • 117
  • Thanks for your answer! Unfortunately I am using C++ 11. – cowculus Aug 14 '18 at 09:21
  • Ok, too bad. I'll keep the answer for possible future reference. – lubgr Aug 14 '18 at 09:25
  • @lubgr "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 reference – phön Aug 14 '18 at 09:43
  • @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. – lubgr Aug 14 '18 at 09:45
  • @lubgr 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 tho – phön Aug 14 '18 at 09:47
  • 1
    @phön The deduction guide turns `F&&` into a forwarding reference. Only without the deduction guide, it's an rvalue reference. See [here](https://en.cppreference.com/w/cpp/language/class_template_argument_deduction) (at the bottom). – lubgr Aug 14 '18 at 09:50
  • @lubgr 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. – Holt Aug 14 '18 at 10:00
  • @lubgr Under https://en.cppreference.com/w/cpp/language/class_template_argument_deduction 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:" – phön Aug 14 '18 at 10:05
  • 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; }` – aschepler Aug 14 '18 at 10:12
  • @phön a forwarding reference *is* "an rvalue ref with reference collapsing". You just get `LambdaClass` when you `Sometype some; LambdaClass lambda(some);`, instead of `LambdaClass` when you `LambdaClass lambda(Sometype{});` – Caleth Aug 14 '18 at 10:12
  • 1
    @lubgr you may want to add `std::remove_reference_t` or `std::decay_t` to the deduction guide – Caleth Aug 14 '18 at 10:14
  • @Caleth Thanks, I was thinking about a possible mitigation. Seems the way to go. – lubgr Aug 14 '18 at 10:15
  • @Caleth 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. – phön Aug 14 '18 at 10:26
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.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
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.

YSC
  • 38,212
  • 9
  • 96
  • 149
  • 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. – Max Langhof Aug 14 '18 at 09:43
  • 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. – Max Langhof Aug 14 '18 at 09:46
  • @MaxLanghof Yes. When you change a class constructor, you should be ready to change the way it's called. The [Open/Close principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) of [SOLID](https://en.wikipedia.org/wiki/SOLID) advises that a software entity should be open for extension, but closed for modification. Making a constructor `explicit` is clearly a modification. – YSC Aug 14 '18 at 09:49
  • 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. – Max Langhof Aug 14 '18 at 09:52
  • 1
    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); }`.) Note that `decltype(f)` can be an lvalue reference type, so `LambdaClass` will have a reference member instead of a class member, which could possibly cause unpleasant surprises. This is why I recommend `LambdaClass::type>` in my answer. – aschepler Aug 14 '18 at 10:19
  • 1
    @YSC Sorry but that's a terrible reason. By that logic, we should all be using [golfing languages](https://codegolf.stackexchange.com/) or only use single letter variable names. Because clearly that improves readability. – Max Langhof Aug 14 '18 at 10:44
  • 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. – Max Langhof Aug 14 '18 at 11:12