More Powerful Lambdas with C++20

Contents[Show]

Thanks to C++20, lambdas become more powerful. From the various lambda improvements, template parameters for lambdas are my favorite ones.

 TimelineCpp20CoreLanguageLambdas support with C++20 template parameters, can be default-constructed and support copy-assignment, when they have no state, and can be used in unevaluated contexts. Additionally, they detect when you implicitly copy the this pointer. This means a significant cause of undefined-behavior with lambdas is gone.

Let's start with template parameters for lambdas.

Template Parameter for Lambdas

Admittedly, the differences between typed lambdas, generic lambdas, and template lambdas (template parameter for lambdas) are subtle.

Four lambda variations

The following program presents four variations of the add function using lambdas for their implementation.

 

// templateLambda.cpp

#include <iostream>
#include <string>
#include <vector>

auto sumInt = [](int fir, int sec) { return fir + sec; };            // only to int convertible types    (C++11)
auto sumGen = [](auto fir, auto sec) { return fir + sec; };          // arbitrary types                  (C++14)
auto sumDec = [](auto fir, decltype(fir) sec) { return fir + sec; }; // arbitrary, but convertible types (C++14)
auto sumTem = []<typename T>(T fir, T sec) { return fir + sec; };    // arbitrary, but identical types   (C++20)

int main() {
    
    std::cout << std::endl;
                                                                            // (1)
    std::cout << "sumInt(2000, 11): " << sumInt(2000, 11) << std::endl;  
    std::cout << "sumGen(2000, 11): " << sumGen(2000, 11) << std::endl;
    std::cout << "sumDec(2000, 11): " << sumDec(2000, 11) << std::endl;
    std::cout << "sumTem(2000, 11): " << sumTem(2000, 11) << std::endl;
    
    std::cout << std::endl;
                                                                            // (2)
    std::string hello = "Hello ";
    std::string world = "world"; 
    // std::cout << "sumInt(hello, world): " << sumInt(hello, world) << std::endl; ERROR
    std::cout << "sumGen(hello, world): " << sumGen(hello, world) << std::endl;
    std::cout << "sumDec(hello, world): " << sumDec(hello, world) << std::endl;
    std::cout << "sumTem(hello, world): " << sumTem(hello, world) << std::endl;
    
    
    std::cout << std::endl;
                                                                             // (3)
    std::cout << "sumInt(true, 2010): " << sumInt(true, 2010) << std::endl;
    std::cout << "sumGen(true, 2010): " << sumGen(true, 2010) << std::endl;
    std::cout << "sumDec(true, 2010): " << sumDec(true, 2010) << std::endl;  
    // std::cout << "sumTem(true, 2010): " << sumTem(true, 2010) << std::endl; ERROR
    
    std::cout << std::endl;
    
}

Before I show the presumably astonishing output of the program, I want to compare the four lambdas.

  • sumInt
    • C++11
    • typed lambda
    • accepts only to int convertible type
  • sumGen
    • C++14
    • generic lambda
    • accepts all types
  • sumDec
    • C++14
    • generic lambda
    • the second type must be convertible to the first type
  • sumTem
    • C++20
    • template lambda
    • the first type and the second type must be the same

What does this mean for template arguments with different types? Of course, each lambda accepts int's (1), and the typed lambda sumInt does not accept strings (2).

Invoking the lambdas with the bool true and the int 2010 may be surprising (3).

  • sumInt returns 2011 because true is integral promoted to int.
  • sumGen returns 2011 because true is integral promoted to int. There is a subtle difference between sumInt and sumGen, which I present in a few lines.
  • sumDec returns 2. Why? The type of the second parameter sec becomes the type of the first parameter fir: thanks to (decltype(fir) sec), the compiler deduces the type of fir and makes it to the type of sec. Consequently, 2010 is converted to true. In the expression fir + sec, fir is integral promoted to 1. Finally, the result is 2.
  • sumTem is not valid.

Thanks to the Compiler Explorer and GCC, here is the output of the program.

templateLambda

There is an interesting difference between sumInt and sumGen. The integral promotion of the true value happens in case of sumInt on the caller-side, but the integral promotion of the true value happens in case of sumGen in the arithmetic expression fir + sec. Here is the essential part of the program once more

 

auto sumInt = [](int fir, int sec) { return fir + sec; };            
auto sumGen = [](auto fir, auto sec) { return fir + sec; };         

int main() {
  
  sumInt(true, 2010);
  sumGen(true, 2010);
  
}

 

When I use the code snippet in C++ Insights (link to the program) it shows the difference. I only show the crucial part of the compiler-generated code.

 

class __lambda_1_15
{
  public: 
  inline /*constexpr */ int operator()(int fir, int sec) const
  {
    return fir + sec;
  }
  
};

__lambda_1_15 sumInt = __lambda_1_15{};
            

class __lambda_2_15
{
  public: 
  template<class type_parameter_0_0, class type_parameter_0_1>
  inline /*constexpr */ auto operator()(type_parameter_0_0 fir, type_parameter_0_1 sec) const
  {
    return fir + sec;
  }
  
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline /*constexpr */ int operator()(bool fir, int sec) const
  {
    return static_cast<int>(fir) + sec;                  // (2)
  }
  #endif
  
};

__lambda_2_15 sumGen = __lambda_2_15{};
         

int main()
{
  sumInt.operator()(static_cast<int>(true), 2010);       // (1)
  sumGen.operator()(true, 2010);
}

 

I assume you know that the compiler generates a function object out of a lambda. If you don't know it, Andreas Fertig wrote a few posts about his tool C++ Insights on my blog. One post is about lambdas: C++ Insights posts.

When you carefully study the code-snippet, you see the difference. sumInt performs the integral promotion on the call-side (1) but sumGen does it in the arithmetic expressions (2).

Honestly, this example was very enlightening for me and hopefully, also for you. A more typical use-case for template lambdas is the usage of containers in lambdas.

Template Parameter for Containers

The following program presents for lambdas accepting a container. Each lambda returns the size of the container.

// templateLambdaVector.cpp

#include <concepts>
#include <deque>
#include <iostream>
#include <string>
#include <vector>

auto lambdaGeneric = [](const auto& container) { return container.size(); };  
auto lambdaVector = []<typename T>(const std::vector<T>& vec) { return vec.size(); };
auto lambdaVectorIntegral = []<std::integral T>(const std::vector<T>& vec) { return vec.size(); };

int main() {

    
    std::cout << std::endl;
    
    std::deque deq{1, 2, 3};                     // (1)                 
    std::vector vecDouble{1.1, 2.2, 3.3, 4.4};   // (1)
    std::vector vecInt{1, 2, 3, 4, 5};           // (1)
  
    std::cout << "lambdaGeneric(deq): " << lambdaGeneric(deq) << std::endl;
    // std::cout << "lambdaVector(deq): " << lambdaVector(deq) << std::endl;                  ERROR
    // std::cout << "lambdaVectorIntegral(deq): " << lambdaVectorIntegral(deq) << std::endl;  ERROR

    std::cout << std::endl;
    
    std::cout << "lambdaGeneric(vecDouble): " << lambdaGeneric(vecDouble) << std::endl;
    std::cout << "lambdaVector(vecDouble): " << lambdaVector(vecDouble) << std::endl;
    // std::cout << "lambdaVectorIntegral(vecDouble): " << lambdaVectorIntegral(vecDouble) << std::endl;
    
    std::cout << std::endl;
     
    std::cout << "lambdaGeneric(vecInt): " << lambdaGeneric(vecInt) << std::endl;
    std::cout << "lambdaVector(vecInt): " << lambdaVector(vecInt) << std::endl;
    std::cout << "lambdaVectorIntegral(vecInt): " << lambdaVectorIntegral(vecInt) << std::endl;
    
    std::cout << std::endl;
    
}

 

lambdaGeneric can be invoked with any data type that has a member function size(). lambdaVector is more specific: it only accepts a std::vector. lambdaVectorIntegral uses C++20 concept std::integral. Consequently, it accepts only a std::vector using integral types such as int. To use it, I have to include the header <concepts>. I assume the small program is self-explanatory.

templateLambdaVector

There is one feature in the program templateLambdaVector.cpp, that you have probably missed. Since C++17, the compiler can deduce the type of a class template from its arguments (1). Consequently, instead of the verbose std::vector<int> myVec{1, 2, 3} you can simply write std::vector myVec{1, 2, 3}.

What's next?

My next post will be about the remaining lambda improvements in C++20.

 

Thanks a lot to my Patreon Supporters: Meeting C++, Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Jon Hess, Christian Wittenhorst, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Lakshman, Kuchlong Kuchlong, Avi Kohn, Serhy Pyton, Robert Blanch, Kuma [], Truels Wissneth, Kris Kafka, Mario Luoni, and Neil Wang.

 

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

Contact Me

Modernes C++,

RainerGrimmSmall

 

 

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 5151

Yesterday 5047

Week 5151

Month 144912

All 4595004

Currently are 191 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments