Using lambda expressions and avoiding pits -- Lecture 15 of C++2.0

lambda expression usage and pit avoidance

0 preparation

Basic concepts:

  • lambda expression: a kind of expression;
  • Closures are runtime objects created by lambda. Closures hold copies or references of data according to different capture modes;
  • A closure class is a class that instantiates a closure. Each closure compiler will generate a unique closure class, and the statements in the closure will become the executable instructions of the closure class member functions.

For the use of foundation, please see Blog 1,Blog 2 , this article mainly explains advanced use and pit avoidance.

1 use

1.1 initialized / generalized lambda capture

The benefits of initializing capture can be specified as follows:

  • The name of the member variable in the closure class generated by lambda;
  • An expression to initialize the variable.

Implementation recommendations:

  • In C++11, the initialization capture is simulated through a manually implemented class or std::bind.
  • C++14 uses initialization capture to move objects into closures;

Implementation in C++14:

class TestWidget{
public:
    bool isValidated()const {return true;}
    bool isProcessed() const {return true;}
    bool isArchived() const {return true;}
};

int main(){
//    auto pw = std::make_unique<TestWidget>();
//    //pw configuration modification
//...
//    auto func =[pw = std::move(pw)] {/ / initialize capture
//        return pw->isArchived() && pw->isValidated();
//    };
//If pw is not configured
    auto func = [pw = std::make_unique<TestWidget>()]{
        return pw->isArchived() && pw->isValidated();
    };
    }

Since there is no way to put the move only object [such as std::forward, std::unique_ptr] into the closure in C++11, there is no way to replace the copy operation with a cheap move operation.

The code of C++11 to realize the above case is:

class TestWidget{
public:
    bool isValidated()const {return true;}
    bool isProcessed() const {return true;}
    bool isArchived() const {return true;}
};

class IsValiAndArch{
public:
    using DataType = std::unique_ptr<TestWidget>;
    explicit IsValiAndArch(DataType&& ptr):pw(std::move(ptr)){}
    bool operator()() const{return pw->isValidated() && pw->isArchived();}
private:
    DataType pw;
};

int main(){
    auto func = IsValiAndArch(std::make_unique<TestWidget>());
    }

If you want to simulate the above mobile capture with lambda, you can use the following method:

  • 1. Move the captured object to the function object generated by std::bind;
  • 2. Give lambda a reference to the object to be captured
    auto func = std::bind([](const std::unique_ptr<TestWidget> pw){return pw->isValidated() && pw->isArchived();},
                          std::make_unique<TestWidget>());

The reason why const is added to the above variables is that by default, the operate() function that generates a closure by lambda will have const, so all member variables in the closure have const modifiers in the lambda expression. If the lambda declaration has mutable, the operate() function that generates a closure will not have const.

1.1.1 std::bind supplement

std::bind Function Description:

  • The first parameter of std::bind is the callable object, and all the next arguments represent the values passed to the modified object;
  • The function object returned by std::bind is a bind object;
  • Binding objects use copies of arguments (stored by value). If you want to store arguments by reference, you can implement std::Ref();
  • Since all arguments of the bound object are passed by reference, in the bound object, the left value argument implements the copy structure, and the right value argument implements the move structure;

The following example is a supplementary example:

//Enable mobile capture

std::vector<double> data;
int main(){
    //C++14
    auto func = [data = std::move(data)]{
        //data operation
    };
    //C++11
    auto func2 = std::bind([](const std::vector<double>& data){
        //Operation on data
        }, std::move(data));
        }

//The implementation is bound to an object with a templated function call operator

class PolyWidget{
public:
    template<typename T>
    void operator()(const T& param) const{}
};


int main(){
    PolyWidget pw;
    auto boudPW = [pw](const auto& param){pw(param);};
    boudPW(1930);
    boudPW(nullptr);
    boudPW("hahahah");

    PolyWidget pw2;
    auto boudPW2 = std::bind(pw2, std::placeholders::_1);
    boudPW2(1930);
    boudPW2(nullptr);
    boudPW2("hahahah");
    }

Suggestions for using std::bind:

  • Only use std::bind in C++11 to implement mobile capture, or bind to objects with templated function call operators.
  • Compared with std::bind, lambda has good readability, strong expressiveness and high efficiency.

The following examples will be used to illustrate these advantages:

The called function is used to give an alarm s at the set time point t and last for d:

//Indicates the type of time
using Time = std::chrono::steady_clock::time_point;
//Sound type
enum class Sound{Beep, Siren, Whistle};
//Type indicating duration
using Duration = std::chrono::steady_clock::duration;

//Set alarm
void setAlarm(Time t, Sound s, Duration d){
}
  • Readability:

Under the condition of C++14, the alarm is sent out after one hour and lasts for 30s by using lambda expression:

int main(){
//Call version 1(C++11)
    auto setSoundL = [](Sound s){
        using namespace std::chrono;
        setAlarm(steady_clock::now() + hours(1), s, seconds(30));
    };
    setSoundL(Sound::Beep);
//Call version 2(C++14)    
    //Simplified call
    auto setSoundL2 = [](Sound s){
        using namespace std::chrono;
        using namespace std::literals;//Import suffixes implemented in C++14
        setAlarm(steady_clock::now() + 1h, s, 30s);
    };
    setSoundL2(Sound::Siren);
    }

Under the condition of C++14, std::bind is used to achieve the same function, but there are still problems in the following code. Because steady_clock::now() is passed to the binding object as an argument, the alarm start time is one hour after calling std::bind instead of one hour after calling setAlarm. [the _1in the code represents a placeholder for the parameter passed to std::bind, that is, the first parameter passed to std::bind will be passed into the setAlarm function as the second parameter]

int main(){
    using namespace std::literals;
    using namespace std::chrono;
    using namespace std::placeholders;
    auto setSoundB = std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);
    setSoundB(Sound::Siren);
    }

Overwrite with std::bind to Version (C++14 version):

	 using namespace std::literals;
    using namespace std::chrono;
    using namespace std::placeholders;
    auto setSoundB2 =
            std::bind(setAlarm,
                  std::bind(std::plus<>(), steady_clock::now(), 1h),
                  _1,
                  30s);
    setSoundB2(Sound::Siren);

Overwrite with std::bind to Version (C++11 version) [there is an error at present]:

    auto setSoundB3 =
        std::bind(setAlarm,
                  std::bind(std::plus<steady_clock::time_point>(),
                            steady_clock::now(),
                            hours(1)),
                            _1,
                            seconds(30));
    setSoundB3(Sound::Siren);

Another example is to find out whether the actual parameter is between the minimum and maximum:

lambda version:

    int lowVal = 1;
    int highVal = 100;
    //lambda
    //c++14 version
    auto betweenL = [lowVal, highVal](const auto & val){
        return lowVal <= val && highVal >= val;
    };
    //C++11 version
    auto betweenL2 = [lowVal, highVal](int val){
        return lowVal <= val && highVal >= val;
    };

    std::cout<<betweenL(20)<<std::endl;
    std::cout<<betweenL2(20)<<std::endl;

The std::bind version is as follows, which is obviously much more complex:

    int lowVal = 1;
    int highVal = 100;
    //c++14
    auto betweenB = std::bind(std::logical_and<>(),
                              std::bind(std::less_equal<>(), lowVal, std::placeholders::_1),
                              std::bind(std::less_equal<>(), std::placeholders::_1 ,highVal));
    std::cout<<betweenB(20)<<std::endl;
    //c++11
    auto betweenB2 = std::bind(std::logical_and<bool>(),
                              std::bind(std::less_equal<int>(), lowVal, std::placeholders::_1),
                              std::bind(std::less_equal<int>(), std::placeholders::_1 ,highVal));
    std::cout<<betweenB2(20)<<std::endl;
  • Strong expressiveness (such as overloaded functions)

Called function:

//Indicates the type of time
using Time = std::chrono::steady_clock::time_point;
//Sound type
enum class Sound{Beep, Siren, Whistle};
//volume
enum class Volume { Normal, Loud, LoudPlusPlus };
//Type indicating duration
using Duration = std::chrono::steady_clock::duration;

void setAlarm(Time t, Sound s, Duration d){}
//function overloading
void setAlarm(Time t, Sound s, Duration d, Volume v){}

lambda style is easy to solve. lambda version runs normally.

    auto setSoundL2 = [](Sound s){
        using namespace std::chrono;
        using namespace std::literals;//Import suffixes implemented in C++14
        setAlarm(steady_clock::now() + 1h, s, 30s);
    };
    setSoundL2(Sound::Siren);

However, the std::bind version of the code has a compilation error (error: no matching function for call to 'bind') because it does not know which version of setAlarm to select. The solution is to use cast.

    using namespace std::literals;
    using namespace std::chrono;
    using namespace std::placeholders;
    using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
    auto setSoundB2 =
            std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
                      std::bind(std::plus<>(), steady_clock::now(), 1h),
                      _1,
                      30s);
    setSoundB2(Sound::Siren);
  • operating efficiency

Because lambda is a regular function call, the compiler will probably inline it, but std::bind calls the function pointer to setAlarm. Because the compiler does not inline the function call initiated through the function pointer, the probability of std::bind calls will not be inline.

1.2 use decltype for auto & & type parameters and pass it with std::forward

The example is as follows. The following code uses decltype to infer the type of x, because T in the template cannot be used, but decltype can infer the type of x.

template<typename T>
void normalize(T param){}

template<typename T>
void func(T param){}

//lambda generated closure class
class ClosureClass{
public:
    template<typename T>
    auto operator()(T x) const{
        return func(normalize(x));
    }
};

int main(){
    auto f = [](auto&& x){
        return func(normalize(std::forward<decltype(x)>(x)));
    };
    }

Supplementary knowledge:

decltype:

  • If an lvalue is passed in, an lvalue reference will be generated. If an lvalue is passed in, an lvalue reference will be generated.

std::forward:

  • If the passed parameter is an lvalue, the lvalue referenced parameter will be generated. If the passed parameter is an lvalue, the lvalue referenced parameter will be generated.
  • If the parameter type is an lvalue reference, it means you want to return an lvalue; if the parameter type is a non reference type, it means you want to return an lvalue.

If the decltype is bound with an R-value, an R-value reference will be generated instead of the R-value generated by the std::forward convention. However, after being forwarded by std::forward, the final result is the same.

forward source code:

template<typename T>
T&& forward(typename std::remove_reference<T>::type& param){
    return static_cast<T&&>(param);
}
//When T takes TestWidget

TestWidget&& forward(TestWidget& param){
    return static_cast<TestWidget&&>(param);
}

//When T takes testwidget & &
TestWidget&& && forward(TestWidget& param){
    return static_cast<TestWidget&& &&>(param);
}
//Reference folded results
TestWidget&& forward(TestWidget& param){
    return static_cast<TestWidget&&>(param);
}



2 precautions

2.1 avoid default capture mode

  • 1. If captured by reference, it may lead to dangling references.

Explanation: capturing by reference will cause the closure to contain references to local variables or formal parameter references within the scope of defining a lambda expression. Once the closure created by the lambda expression exceeds the declaration cycle of the local variable or formal parameter, the reference of the closure will be suspended.

In the following example, the lambda expression is used and the reference of the local variable divisor is used. After leaving the scope of addDivisorFilter, the divisor is destroyed, resulting in the suspension of the reference. The wrong value also appears in the call of filters[0](12) in the main function.

int computerSomevalue1(){return 2;}
int computerSomevalue2(){return 3;}
int computerDivisor(int v1, int v2){return v1 + v2;}

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;
void addDivisorFilter(){
    auto calc1 = computerSomevalue1();
    auto calc2 = computerSomevalue2();
    auto divisor = computerDivisor(calc1, calc2);
    filters.emplace_back([&](int value){//Multiples of filter divisor
        return value % divisor == 0;
    });
}

int main(){
    addDivisorFilter();
    std::cout<<filters[0](12)<<std::endl;//1
    std::cout<<(12 % 5)<<std::endl;//0
    }

The solution is to make the declaration cycle of local variables or formal parameters longer. However, if the lambda expression is copied and pasted into other closures (the life cycle is longer than that of the divisor), the same problem will also occur.

template<typename C>
void workWithContainer(const C& container){
    auto calc1 = computerSomevalue1();
    auto calc2 = computerSomevalue2();
    auto divisor = computerDivisor(calc1, calc2);

    using containerType = typename C::value_type;
    using std::begin;
    using std::end;

    if(std::all_of(begin(container), end(container), [&](const containerType& value)
    {return value % divisor == 0;})
        ){
        //If the elements in the container are multiples of the division
        std::cout<<"All container satisfy";
    }else{
        std::cout<<"Not all container satisfy";
    }
}


int main(){

    std::vector<int> v{2, 5, 10};
    workWithContainer(v);
    }

Another solution is to use value capture instead, as shown below.

    filters.emplace_back([=](int value){
        return value % divisor == 0;
    });
  • 2. The default capture by value is easily affected by the null pointer (especially this) and misleads people to think that it is self consistent [not affected by the change of data outside the closure].

In the following code, you cannot capture non static local variables (including formal parameters) that can only be visible for creating a lambda scope, and the divisor is a member variable of the TestWidget.

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;

class TestWidget{
public:
    void addFilter()const;
private:
    int divisor;
};

void TestWidget::addFilter() const {
    filters.emplace_back([](int value){
       return value % divisor == 0;
    });
}

int main(){
    TestWidget w;
    w.addFilter();
    }

It cannot be captured as follows, because the divisor is neither a local variable nor a formal parameter.

void TestWidget::addFilter() const {
    filters.emplace_back([divisor](int value){
       return value % divisor == 0;
    });
}

In the compiler's view, the above code is equivalent to the following code, because each non static member function holds a this pointer. Whenever the member variable of this class is used, the this pointer will be used, that is, the this pointer of the captured TestWidget, not the divisor.

void TestWidget::addFilter() const {
	auto currentObjectPtr = this;
    filters.emplace_back([divisor](int value){
       return value % currentObjectPtr->divisor == 0;
    });
}

By assigning the variables to be captured to local variables, the program can also run normally, or capture using generalized lambda.

void TestWidget::addFilter() const {
    auto divisorCopy = divisor;
    filters.emplace_back([=](int value){
        return value % divisorCopy == 0;
    });
}

//Generalized lambda capture
void TestWidget::addFilter() const {
    filters.emplace_back([divisor=divisor](int value){
        return value % divisor == 0;
    });
}

The following code uses lambda default value capture. People may think that closures are insulated from data changes outside closures, but lambda can also use static storage objects (i.e. objects decorated with static). Although such objects can be used by lambda, they cannot be captured. That is, from the result, I wanted to find a multiple of 5, but the static variable was accidentally modified, so it became a multiple of 6.

int computerSomevalue1(){return 2;}
int computerSomevalue2(){return 3;}
int computerDivisor(int v1, int v2){return v1 + v2;}

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;

void addDivisorFilter(){
    static auto calc1 = computerSomevalue1();
    static auto calc2 = computerSomevalue2();
    static auto divisor = computerDivisor(calc1, calc2);

    filters.emplace_back([=](int value){
        return  value % divisor == 0;
    });
    divisor++;
}

int main(){
    addDivisorFilter();
    std::cout<<filters[0](10)<<std::endl;
    std::cout<<filters[0](12)<<std::endl;
    }

Tags: C++ Lambda

Posted on Thu, 25 Nov 2021 17:09:13 -0500 by coffeecup