본문 바로가기

3.구현/C or C++

[c++] 람다식 사용하기

들어가기

가장 유용하지만 가능 신경써야할게 많은 람다식이다. C++14에서는 auto로 인해 더 사용이 용이해졌다. 그러나 사용할 때 주의해야할 점이 있다. 하나씩 살펴보자.

작성자: ospace114@empal.com, http://ospace.tistory.com/

람다식

람다식의 기본형은 다음과 같다.

["capture 절"]("인자목록") "추가사양" "예외사양" -> "반환형식" { "몸체" } 
  • capture 절: 해당 범위에서 액세스 또는 복사할 변수를 지정한다. 만약 없다면 “[]”만 사용하면 된다. 또한 기본 모드를 지정할 수 있다. “[&]”을 사용해 해당 범위의 모든 변수를 참조할 수 있고 “[=]”를 사용해 모든 변수를 복사해서 값을 사용할 수 있다. 또는 개별로 지정하거나 기본 모드와 혼합해서 사용할 수 있다. 개별 지정시 “=”은 명시할 필요가 없다.
[&val1, val2]
[&, var1]
[=, &var2]
  • 인자목록: 함수에서 사용하는 인자목록과 동일하다.
  • 추가사향: 선택항목이다. mutable를 사용하면 본문에서 값으로 캡처된 변수를 수정할수 있다.
  • 예외사양: 예외 발생에 대한 지정을 할 수 있다. noexcept로 예외발생하지 않음을 명시할 수도 있다.
  • 반환형식: 선택항목으로 몇시적으로 형식을 지정할 때에 사용된다. 미사용시 return 구문에 의해 자동 추론으로 반환 형식이 결정된다.
  • 몸체: 실행 문장이 포함된다.

자세한 내용은 참고 ”C++ 람다 식“[2]을 참고하시 바란다.

capture 절 사용

보통 람다식은 임시로 일시적으로 사용하는 경우에는 크게 문제가 없다. 그러나 추후에 호출할 목적으로 수명이 오래 지속되는 경우가 있다. capture 절에서 참조로 변수를 접근하는 경우 해당 변수 수명이 람다식보다 짧을 경우 문제가 발생한다. 람다식이 실행할 시점에 해당 변수가 없을 경우 참조 실패가 되거나, 해당 변수의 데이터 변경으로 잘못된 접근이 발생할 가능이 있기 때문에 주의가 필요하다.

그렇기 때문에 기본 capture 모드를 지정할 경우 해당 람다식이 참조하는 변수를 알 수 없기에 가급적 참조하는 변수를 명시하는게 좋다.

template<typename C>
void processContainer(const C& c) {
    using ElemT = typename C::value_type;
    auto divisor = computeDivisor();
    if (std::all_of( std::begin(c), std::end(c),
        [&divisor](const auto& value) { return 0 == value % divisor;}
  )) {
    //...
  }
}

클래스에서 람다식을 사용할 때에 capture 절의 범위를 신경써야 한다.

class Widget {
public:
  void addFilter() const {
      filters.emplace_back(
      [divisor](int value) { return 0 == value & divisor; }
    );
  }
private:
  int divisor;
};

addFilter()의 람다식은 divisor은 해당 함수 범위에 있지 않고 this에 있다. 즉, this->divisor으로 접근해야 한다. addFilter()가 다음처럼 수정해야 한다.

void addFilter() const {
  auto self = this;
  filters.emplace_back(
    [self](int value) { return 0 == value & self->divisor; }
  );
}

C++14에서는 좀더 일반화된 capture 방법이 있다.

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

capture 절을 값에 의한 기본 capture 모드를 사용하면 자기 완결적으로 외부 영향이 없을 거라 생각할 수 있다.

template<typename C>
void processContainer(const C& c) {
  using ElemT = typename C::value_type;
  static auto divisor = computeDivisor();
  filters.emplace_back(
    [=](int value) { return 0 == value % divisor }
  );
  ++divisor;
}

static 변수인 divisor은 람다식 추가 후에 증가한다. 실제 람다식 실행 시점에는 증가된 divisor를 사용한다. 값에 의한 captue에 대한 잘못된 인식으로 잠재적 문제가 생겼다.

초기화에 의한 capture

C++14에서 지원하는 기능으로 “=”을 사용해서 capture 대상으로 초기화해서 접근하는 방식이다. 이를 일반화된 람다 캡처(generalized lambda capture)라고 한다.

auto func = [divisor = divisor](int value) { return 0 == value % divisor; }

등호(=) 왼쪽이 람다식에 사용할 변수이고 오른쪽은 람다식이 정의되는 범위에서 변수이다.

C++11에서는 미지원하는 기능으로 bind()을 사용해서 비슷하게 흉내낼 수 있다.

auto func = std::bind(
  [](int divisor, int value) { return 0 == value % divisor; },
  divisor,
  std::placeholders::_1
);

std::bind()가 리턴하는 함수 객체를 바인드 객체라고 한다. std::bind()는 기본적으로 인수를 값으로 저장했다가 전달한다. std::placeholders::_1는 func 호출할 때에 첫번째 인자를 대처한다는 의미이다.

std::bind으로 할수 있다고 해도 C++14이상이면 람다식을 선택하는게 더 직관적이다. 그리고 bind가 deprecated가 되었다는 말이 있지만, 아직 공식적인것 같지 않다. 그래도 C++14 이상에서는 이미 작성된 경우가 아니라면 새로 작업할 때에는 람다식이 좀더 유리하다고 생각이 든다. C++11 이하에서는 bind가 유용한 부분도 많이 있으니 잘 판단해서 사용하면 좋을 듯 한다.

auto 오른쪽 참조

C++14에서 람다식 인자에 auto 형식을 사용할 수 있게되면서 상당히 간소화되었다. 일반적인 auto 형식은 왼쪽값을 받는다.

auto fwd = [](auto x) { return f(x); };

보편참조로 완벽전달을 사용하고 싶다면 다음 처럼 사용하면 된다.

auto fwd = [](auto&& x) { return f(std::forward<decltype(x)>(x)); }

인자형식은 보편 참조 형식({타입}&&)을 사용하면 된다. std::forward에서는 템플릿 인자가 없기에 타입정보를 전달할 수 없다. decltype()을 사용하여 입력 인자를 통해서 형식 정보를 획득하면 된다.

마무리

너무 복잡해지거나, 없어도 문제가 없는 부분을 과감히 생략했다. 그리고 예제도 통일성있게 수정했다. 그리도 내용 변경도 있다. 뭔가 이상하라는 부분의 저의 잘못일 수 있습니다. 세부적인 내용을 알고 싶다면 참고[1]에 책을 읽어보시기를 권합니다. 부족한 글이지만 여러분에게 도움이 되었으면 합니다. 즐거운 코딩생활 되세요. ospace.

참고

[1] 스콧 마이어스, Effective Modern C++, 인사이트

[2] C++ 람다 식, https://learn.microsoft.com/ko-kr/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170

반응형