티스토리 뷰

 람다식의 관련된 문제로 여기서 말하는 갈무리는 캡쳐절을 주의하라는 의미이다. 아래 링크를 참고하자.

 

C++의 람다 식

C++의 람다 식Lambda Expressions in C++ 이 문서의 내용 --> C + + 11 이상에서 람다 식 (종종 람다)은 호출 되거나 함수에 인수로 전달 되는 위치에서 무명 함수 개체 ( 클로저)를 정의 하는 편리한 방법입니다.In C++11 and later, a lambda expression—often called a lambda—is a convenient way of defining an anonymous function objec

docs.microsoft.com

기본 갈무리 모드는 두가지로 나눌 수 있다. 하나는 참조에 의한 갈무리 모드와 값에 의한 갈무리 모드를 의미한다. 일반적으로 참조에 의한 갈무리 모드가 위험하다는 것을 알지만 값에 의한 갈무리 모드가 위험하다는 것을 인지하지 못한다. 두가지 방법이 어떤식으로 문제를 야기하는지 알아보도록 하자.

참조에서의 문제

참조에서 발생하는 문제는 간단하다. 지역 변수를 참조로 캡쳐한 상황에서 클로저보다 지역 변수가 먼저 소멸되는 경우 참조를 잃게 되는 문제이다. 코드로 살펴보자.

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

void addDivisorFilter()
{
  auto calc1 = computeSomeValue1();
  auto calc2 = computeSomeValue2();
  
  auto divisor = computeDivisor(calc1, calc2);
  
  filters.emplace_back(
    [&](int value) { return value % divisor == 0; } // 위험
  );
}

 위 코드의 람다식에서 사용하는 divisoraddDivisorFilter 함수의 지역변수이다. 문제는 람다 함수가 실행되는 시점보다 함수가 먼저 끝나버리기 때문에 divisor 는 소멸되어 버린다. 람다 함수가 실행되는 시점에는 소멸된 변수에 접근하게 되어 문제가 발생하게 된다. 이 문제는 기본 참조 갈무리가 아닌 명시적으로 지정해도 문제가 발생한다.

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

 클로저를 즉시 사용하는 것이 가장 깔끔하게 해결 할 수 있다.

template<typename C>
void workWithContainer(const C& container)
{
  auto calc1 = computeSomeValue1();
  auto calc2 = computeSomeValue2();
  
  auto divisor = computeDivisor(calc1, calc2);
  
  using std::begin;
  using std::end;
  
  if (std::all_of(
        begin(container), end(container),
        [&](const auto& value)
        { return value % divisor == 0; })
      ) {
    ...
  } else {
    ...
  }
}

  위 코드가 안전하지만 다른 코드에서는 안전성이 깨질 수 있다. 참조 대신 값을 사용하면 어떨까?

기본 값에서의 문제

 아래 코드로 앞서 말한 문제를 해결 할 수 있다.

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

 기본 값 갈무리 모드를 사용하면 위 문제는 안전하기는 하다. 그러나 예상만큼 안전하지는 않다. 아래의 코드를 살펴보자.

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

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

 위 코드를 살펴봤을때는 마치 문제가 없는 것처럼 보인다. 아래의 코드를 살펴보자.

void Widget::addFilter() const
{
  filters.emplace_back(
    [](int value) { return value % divisor == 0; } // 컴파일 오류
  );
}

void Widget::addFilter() const
{
  filters.emplace_back(
    [divisor](int value)                           // 컴파일 오류
    { return value % divisor == 0; }
  );
}

 위 코드에서 컴파일 오류가 발생하는 이유는 무엇일까?

그 이유는 캡쳐에 사용할 수 있는 범위는 static 이 아닌 지역 변수에만 적용되기 때문이다. divisor 는 지역 변수가 아니기 때문에 문제가 발생하는 것이다.

그렇다면 기본 갈무리 모드에서 문제가 없었던 이유는 무엇일까?

바로 this 포인터 때문이다. 복사가 되는 값은 바로 this 이기 때문이다. 그럼 똑같은 문제가 발생한다. 클로저가 사용되기 전에 this 포인터가 소멸될 수 있기 때문이다. 다행히도 해결 방법은 간단하다. divisor 를 지역 변수로 복사하면 된다.

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

// C++14
void Widget::addFilter() const
{
  filters.emplace_back(
    [divisor = divisor](int value)
    { return value % divisorCopy == 0; }
  );
}

위의 방법으로 기본 값 갈무리도 사용하지 않고 this 포인터가 소멸되도 문제가 생기지 않게 되었다. 마지막으로 static 을 사용하는 경우를 살펴보자.

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 는 복사본이라고 생각할 수도 있겠다. 그러나 앞서 말했듯이 static 이 아닌 지역변수만 캡쳐가 가능하기 때문에 위에서 사용하는 divisor 는 복사본이 아니다. 여기서 사용하는 divisorstatic 으로 선언된 divisor 이기 때문이다. 

기본 참조 갈무리와 기본 값 갈무리는 문제를 발생시킬 여지를 만든다. 그러므로 기본 갈무리는 피하는 것이 좋겠다.

 

참고 서적

스콧 마이어스, Effective Modern C++
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함