Home [C++] emc++ 04: Lambda
Post
Cancel

[C++] emc++ 04: Lambda

Lambda

1
std::find_if(container.begin(), container.end(), [](int val){ return 0 < val && val < 10; });
  • 람다는 함수 객체를 만드는데 유용한 방법
    • 람다 표현식은 단순한 표현식
      • 소스 코드의 일부
      • [](...){~}
    • 클로저(closure)
      • 람다에 의해 런타임에 생성되는 객체
      • 캡쳐 모드에 따라 클로져는 캡쳐된 데이터의 복사본 또는 레퍼런스를 갖고 있게됨
      • 복사 가능 (auto c1 = [x](int y){return x > 55;});)
    • 클로져 클래스(closure class)
      • 클로져가 인스턴스화된 클래스
      • 각각의 람다는 컴파일러가 고유한 클로져 클래스를 만들게 함
      • 람다 표현식 내부의 문장(statement)들은 그 람다 표현식으로 인해 생성되는 클로져 클래스의 멤버 함수 속의 실행가능한 명령문(instruction)이 됨

항목31: 기본 캡쳐 모드(default capture mode)를 피하라

  • C++11의 람다의 캡쳐모드 두 종류는 잠재적인 문제점을 갖고 있음
    • by-reference
    • by-value

by-reference

  • 댕글링 위험
    • 람다가 정의된 범위에서 사용가능한 지역, 매개 변수들에 대한 레퍼런스를 포함하는 클로져를 생성함
    • 클로져의 생명주기가 지역변수나 매개변수보다 더 길다면, 레퍼런스가 댕글링을 일으킴

ex) 필터함수

1
2
3
4
5
using FilterContainer = std::vector<std::function<bool(int)>>;

FilterContainer filters;

filters.emplace_back([](int value){ return value % 5 == 0; });
  • 특정 변수로 체크하는 경우는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
void addDivisorFilter()
{
    auto calc1 = computeSomeValue1();
    auto calc2 = computeSomeValue2();

    auto divisor = computeDivisor(calc1, calc2);

    filters.emplace_back
    (
        [&](int value) { return value % divisor == 0; }
    );
}
  • [&]에 의해 지역 변수 divisor의 레퍼런스가 클로져에 저장됨
    • filters의 생명주기가 더 길기 때문에 문제됨

by-value

  • 댕글링 문제해결할 방안?
1
2
filters.emplace_back
([=](int value){ return value % divisor == 0; });
  • 그러나, 댕글링 문제에서 벗어날 수 없음
    • 포인터 변수는 주소값을 가지므로, 이를 참조할 때 문제가 생김 (대표적 예: this)
1
2
3
4
5
6
7
8
9
10
void Widget::addFilter() const
{
    auto currentObjectPtr = this;

    filters.emplace_back
    ([currentObjectPtr](int value)
    {
        return value % currentObjectPtr->divisor == 0;
    });
}
  • [=]

    • 해당 람다가 생성된 범위에서 볼 수 있는 static이 아닌 지역변수를 복사 (매개변수 포함)
      • 클래스 멤버 변수들은 캡쳐 못함
    • this를 사용하여 멤버변수에 접근해야함
  • 원하는 멤버변수를 this 대신 지역변수에 담아 복사하면됨

    • C++14의 일반화된 람다 캡쳐([divisor = divisor])

by-value 문제점

  • 해당 클로져가 독립적이고, 외부 데이터의 변화로부터 어떤 영향도 받지 않을 것처럼 보이기 때문

  • 사실은 “지역변수”, “매개변수”, “정적 객체(static object)”들로부터도 영향을 받음

    • 외부에 완전히 독립적이지 않음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addDivisorFilter()
{
    static auto calc1 = computeSomeValue1();
    static auto calc2 = computeSomeValue2();

    static auto divisor =
        computeDivisor(calc1, calc2);

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

    ++divisor;
}
  • 정적 변수
    • 캡쳐가 아니라 외부의값을 그저 가져다 쓰는것
    • divisor 값이 변하면 람다 내부의 결과도 변함

항목32: 객체를 클로저 안으로 이동하려면 init capture를 사용하라

  • move-only(unique_ptr, future) 객체를 클로져 내부로 이동시키고 싶은 경우
    • C++11에서는 방법이 없음
    • C++14에서는 클로져 내부로 객체를 이동시키는 방법을 제공함

C++14: init capture

  • init capture
    • 외부의 객체를 클로져 내부로 이동시킴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Widget
{
public:
    bool isValidated() const;
    bool isProcessed() const;
    bool isArchived() const;

private:
};

auto pw = std::make_unique<Widget>();

//closure 내부의 멤버 pw를 std::move(pw)로 초기화
auto func = [pw = std::move(pw)]
            { return pw->isValidated()
                     && pw->isArchived(); };
  • 사용

    1. 람다로부터 생성되는 클로져 클래스 내부의 데이터 멤버 이름 기술
    2. 해당 데이터 멤버를 초기화하기 위한 표현식(expression) 기술
  • [클로져 클래스 내부의 멤버 이름 = 내부 멤버를 초기화하기 위한 표현식]

    • 내부 멤버 초기화 가능

C++11

직접 함수 객체를 구현 (Functor)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class IsValAndArch
{
public:
    using DataType = sstd::unique_ptr<Widget>;

    explicit IsValAndArch(DataType&& ptr)
    : pw(std::move(ptr)){}

    bool operator()() const
    { return pw->isValidated() && pw->isArchived(); }

private:
    DataType pw;
};

auto func = IsValAndArch(std::make_unique<Widget>());

std::bind 응용

  1. 캡쳐할 객체를 std::bind로 생성된 함수 객체로 이동
  2. 람다에 캡쳐된 객체에 대한 레퍼런스를 주기
1
2
3
4
5
6
7
8
9
10
11
// C++14의 경우
std::vector<double> data;
auto func = [data = std::move(data)]{};

// C++11의 경우
std::vector<double> data;

auto func =
    std::bind(
        [](const std::vector<double>& data)
        { }, std::move(data));
  • std::bind

    • 이 역시 함수 객체를 리턴
    • 1번째 인자: 호출 가능한 오브젝트
    • 2번째 인자: 해당 오브젝트로 전달될 값들을 나타냄
  • 람다 표현식으로 인해 클로저 클래스 내부의 operator() 함수는 기본적으로 const

    • 클로저 내부의 모든 데이터 멤버들은 람다의 body에서 const로 취급
    • C++11의 방식인 bind object안에 이동 생성된 data의 복사본은 const가 아님
  • 람다를 mutable로 선언하면, const 제거 가능

1
2
3
4
auto func =
    std::bind(
        [](std::vector<double>& data) mutable
        { }, std::move(data));
  • std::bind 장점: 댕글링 발생 x
    • std::bind는 자신의 인자로 넘어온 모든 인자들을 bind object에 저장
    • 클로져도 내부적으로 저장
    • 클로져의 생명주기와 bind object와 같아짐
1
2
3
4
5
6
7
8
9
10
11
//C++14
auto func = [pw = std::make_unique<Widget>()]
            { return pw->isValidated()
                     && pw->isArchived(); };

//C++11
auto func = std::bind(
              [](const std::unique_ptr<Widget>& pw)
              {return pw->isValidated()
                      && pw->isArchived(); },
              std::make_unique<Widget>());

항목33: std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라

  • C++14의 generic lambda 기능

    • 람다의 매개변수 정의에 auto를 사용가능
      • 이 기능의 구현: 람다의 클로저 클래스 내부 멤버함수 operator()를 템플릿으로 만드는 것으로 이루어짐
  • https://jwvg0425.tistory.com/50

항목34: std::bind보다 람다를 선호하라

  • https://jwvg0425.tistory.com/51

출처

  • https://jwvg0425.tistory.com/48
  • https://jwvg0425.tistory.com/49
This post is licensed under CC BY 4.0 by the author.

[C++] emc++ 04: Move, Perfect forwarding

[C++] emc++ 06: Thread