• Home
  • /
  • Blog
  • /
  • More Powerful Lambdas with C++20

July 31, 2020

Thanks to C++20, lambdas have 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 parameters 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 program’s output.

templateLambda

There is an exciting difference between sumInt and sumGen. The integral promotion of the true value happens in the case of sumInt on the caller side. Still, the integral promotion of the true value happens in the 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, 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 from a lambda. If you don’t know, Andreas Fertig wrote a few posts on my blog about his tool C++ Insights. 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 enlightening for me and, hopefully, for you. A more typical use case for template lambdas is the usage of containers in lambdas.

Template Parameters for Containers

The following program presents 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 with 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 class template type from its arguments (1). Consequently, instead of the verbose std::vector<int> myVec{1, 2, 3}, you can write std::vector myVec{1, 2, 3}.

What’s next?

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

 

 

Leave a Reply


Your email address will not be published. Required fields are marked

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Related Posts