[C++ Core] 함수 개체(function object)와 람다 표현식(lambda expression)
함수 개체
함수 개체(function object)란, operator()
를 오버로드한 개체를 의미한다. 펑터(functor)라고도 한다. 다음은 가장 간단한 함수 개체이다.
struct Foo { void operator()() { } }; // 실제 사용법 Foo a; a();
그럼 이와 같은 함수 개체는 왜 사용하는 것일까? 바로, 성능 때문에 사용하는 것이다. 특히, 함수 개체는 STL 알고리즘을 이용할 때 유용하다. STL 알고리즘의 어떤 버전은 마지막 인자로 함수를 받는다. 가령 std::for_each()
나 std::count_if()
말이다. 여기에 함수 개체를 넘겨주면 그 바디가 인라인(inline) 되어 성능에 상당한 이점을 가져갈 수 있다.
하지만, 함수 개체의 가장 큰 단점은 기술하기 귀찮다는 것이다. 다음을 보자.
void PrintModulo(const std::vector<int>& v, std::ostream& os, int m) { // 해당 함수 개체는 일회성이다. class ModuloPrintFunctor { public: ModuloPrintFunctor(std::ostream& o, int mm) : os{ o }, m{ mm } { } void opreator()(int x) const { if (x & m == 0) { os << x << std::endl; } } private: std::ostream& os; int m; }; std::for_each(v.cbegin(), v.cend(), ModuloPrintFunctor(os, m)); }
해당 함수 개체가 여러 번 사용된다면 모르겠지만, 단 한번의 사용을 위해 기술하긴 여간 귀찮은게 아니다. 이 불편함을 없애기 위해 람다 표현식(lambda expression)이 등장하였다.
람다 표현식
람다 표현식은 익명 함수 개체를 정의하고 사용하기 위한 간단한 표기법이다. 아까의 함수 개체를 람다 표현식으로 기술해보겠다.
void PrintModulo(const std::vector<int>& v, std::ostream& os, int m) { // 아까의 코드보다 짧아지고 명확히 보인다. std::for_each(v.cbegin(), v.cend(), [&os, m](int x) { if (x & m == 0) { os << x << std::endl; } }); }
이렇게 람다로 만들어진 개체를 클로저(closure)라고 한다. 람다는 다음처럼 구성된다.
[capture-list] (parameter-list) mutable noexcept -> return_type { some expression }
기본적으로 함수와 비슷하다는 것을 볼 수 있다. 왜냐하면 람다의 대부분의 규칙은 클래스 및 함수에서 빌려온 것이기 때문이다. 다른 점은 두 가지이다. 첫 번째, 매개변수 리스트는 매개변수가 존재하지 않는다면 생략할 수 있다. 즉, [] { some expression }
이 가능하다. 두 번째, 반환 타입은 반환문에 의해 추론된다. 이 때, 반환문이 단일이 아니라면 반환 타입을 명시적으로 기술하여야 한다. 이 때에는 매개변수 리스트를 생략할 수 없다.
mutable
한정자는 캡처한 변수를 수정할 때 이용된다. 람다를 통해 만들어진 함수는 기본적으로 const
이기 때문에, 수정할 수 없다. 하지만, mutable
한정자를 붙여주면 const
가 아닌 함수가 되어, 수정할 수 있다.
noexcept
한정자는 해당 람다에서는 예외가 발생하지 않는다는 것을 컴파일러에게 알려준다.
캡처 리스트가 람다의 핵심이라고 할 수 있는데, 한번 자세히 살펴보자.
캡처 리스트
캡처 리스트는 어떤 지역 변수에 어떤 방법으로 접근할 수 있는지를 나타내는 리스트로서, []
람다 소개자에 의해 기술된다. 아까의 [&os, m]
이 여기에 해당하는 데, 이는 os
를 참조를 통해 사용하겠다는 의미이고, m
은 값을 통해 사용하겠다는 의미이다. 기본적으로, &
가 붙으면 참조를, 그 외에는 값을 의미한다. 될 수 있는 형태는 다음과 같다.
[]
: 아무 지역 변수도 사용하지 않는다.[&]
: 암시적으로 모든 지역 변수를 참조로 사용한다.[=]
: 암시적으로 모든 지역 변수를 값으로 사용한다.[capture-list]
: 명시적으로 사용할 변수를 나열한다.this
와...
을 사용할 수 있다.[&, 캡처 리스트]
: 명시된 변수를 값으로, 이외는 참조로 캡처한다.this
를 사용할 수 있다.[=, 캡처 리스트]
: 명시된 변수를 참조로, 이외는 값으로 캡처한다.this
를 사용할 수 없다.
this
캡처 리스트에 this
를 쓰는 것은 클래스의 멤버를 캡처하겠다는 뜻이 된다. 예시를 살펴보자.
class Temp { public: Temp(int ii) : i{ ii } { } int GetNumber() const { auto getNumber = [this] { return i; }; return getNumber(); } private: int i; };
C++은 일급(first-class) 함수를 지원하는 언어이기 때문에 람다를 변수에 할당할 수 있다. 람다의 타입은 보통 최적화를 위해 정의되어 있지 않아 auto를 쓴다. 하지만, 필요하다면 C 스타일의 함수 포인터나 std::function
으로 가리킬 수 있다. 이는, 재귀적인 람다 함수를 만들 때 유용하다.
auto lambda = [](int x, int y) { return x + y; }; int (*p)(int, int) = lambda; std::function<int(int, int)> fp = lambda;
다시 돌아와서, 아까의 [this]
예제에서 주의해야 할 것은 멀티 쓰레드 프로그램에서는 경쟁이 일어날 수 있기 때문에, 그때에는 [this]
대신 [=]
을 쓰는 것이 좋을 것이다.
...
가변 템플릿 인자를 캡처하기 위해 ...
을 사용할 수 있다.
template <typename... Arg> void Algorithm(int i, Arg... v) { auto helper = [&i, &v...] { return i * (h1(v...) + h2(v...)); }; // do something }
유의할 점
변수를 캡처할 때, 람다와 캡처된 변수의 수명 주기가 다를 수도 있음에 주의해야 한다.
void Setup(Menu& m) { // ... Point p1, p2, p3; m.add("Draw triangle", [&]{ m.draw(p1, p2, p3); }); // oops // ... }
람다는 호출된 곳 외에서도 살아있을 수 있기 때문에, 저런 상황에서는 참조로 캡처하는 대신 값으로 캡처하는 것이 좋다.
또, 컨테이너의 전체를 순회할 때, 람다 + 알고리즘 조합대신 for 루프를 사용할 수도 있는데 둘의 성능은 비슷하기 때문에 선호하는 쪽으로 사용하면 된다.
'Outdated > Core Language' 카테고리의 다른 글
[C++ Core] 상수 구문 - new 연산자 (0) | 2018.08.29 |
---|---|
[C++ Core] 상수 구문 - const vs constexpr (0) | 2018.08.21 |
[C++ Core] POD(plain old data), 표준 레이아웃 타입(standard layout type), 간단한 타입(trivial type) (0) | 2018.08.17 |
[C++ Core] 우측 값 레퍼런스(rvalue reference) (4) | 2018.08.15 |
[C++ Core] 타입 추론(type deduction) - auto와 decltype (0) | 2018.08.13 |