람다 함수 또는 람다라고도 하는 람다식은 익명 함수 객체를 정의하고 사용하기 위한 단순화된 표기법이다. operator()로 명명된 클래스를 정의하는 대신 나중에 해당 클래스의 객체를 만들고 마지막으로 호출하는 대신 약식을 사용할 수 있다. 이것은 연산을 알고리즘에 인수로 전달하려는 경우에 특히 유용하다. GUI(및 다른 곳)의 context에서 이러한 작업을 종종 callback이라고도 한다. 이 섹션에서는 람다의 기출적 측면에 중점을 둔다. 람다 사용에 대한 예제와 기술은 다른 곳에서 찾을 수 있다.
람다 표현식은 일련의 부분으로 구성된다.
- 람다 식의 본문에 사용할 수 있는 정의 환경의 이름과 참조 복사 또는 액세스 할지 여부를 지정하는 비어 있을 수 있는 capture 목록. capture 목록은 []로 구분된다.
- 람다 식에 필요한 인수를 지정하는 선택적 매개 변수 목록. 매개변수 목록은 ()로 구분된다.
- 람다 식의 본문이 람다의 상태를 수정할 수 있음을 나타내는 선택적 변경 가능한 지정자 (즉, 값으로 capture 된 변수의 람다 복사본 변경)
- 선택적 noexcept 지정자
- ->유형 형식의 선택적 반환 유형 선언
- 실행할 코드를 지정하는 본문. 본문은 {}로 구분된다.
지역 변수의 'capture'라는 개념은 함수에 대해 제공되지 않는다. 이는 함수가 할 수 없는 경우에도 람다가 로컬 함수로 작동할 수 있음을 의미한다.
구현 모델
람다 표현식은 다양한 방식으로 구현될 수 있으며, 이를 최적화하는 다소 효과적인 방법이 있다. 그러나 람다를 함수 객체를 정의하고 사용하기 위한 약어로 간주하는 방식으로 람다의 의미를 이해하는 것이 유용하다.
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for_each(begin(v),end(v),
[&os,m](int x) { if (x%m==0) os << x << '\n'; }
);
}
이것과 동등한 의미를 가지는 함수를 정의할 수 있다.
class Modulo_print {
ostream& os; // members to hold the capture list
int m;
public:
Modulo_print(ostream& s, int mm) :os(s), m(mm) {} // capture
void operator()(int x) const
{ if (x%m==0) os << x << '\n'; }
};
capture list, [&os, m], 은 두 개의 멤버 변수와 이를 초기화하는 생성자가 된다. os 앞에 &는 참조를 저장해야 함을 의미하고 m에 &가 없으면 사본을 저장해야 함을 의미한다. &의 이 사용은 함수 인수 선언에서의 사용을 반영한다.
람다의 본문은 단순히 operator()()의 본문이 된다. 람다는 값을 반환하지 않으므로 operator()()는 void다. 기본적으로 operator()()는 const이므로 람다 본문이 캡처된 변수를 수정하지 않는다. 이것은 가장 일반적인 경우다. 본문에서 람다의 상태를 수정하려는 경우 람다를 변경 가능으로 선언할 수 있다. 이것은 const로 선언되지 않은 operator()()에 해당한다.
람다에서 생성된 클래스의 객체를 클로저 객체(또는 클로저)라고 한다.
다음과 같이 original 함수를 작성할 수 있다.
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for_each(begin(v),end(v),Modulo_print{os,m});
}
람다의 대안
print_modulo()의 최종 버전은 실제로 매우 매력적이며 사소한 작업에 이름을 지정하는 것은 일반적으로 좋은 생각이다. 별도로 정의된 클래스는 일부 인수 목록에 포함된 람다보다 주석을 위한 더 많은 공간을 남긴다.
그러나 많은 람다는 작고 한 번만 사용된다. 그러한 사용을 위해 현실적인 방법은 사용 직전에 정의된 지역 클래스를 포함하는 것이다.
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
class Modulo_print {
ostream& os; // members to hold the capture list
int m;
public:
Modulo_print (ostream& s, int mm) :os(s), m(mm) {} // capture
void operator()(int x) const
{ if (x%m==0) os << x << '\n'; }
};
for_each(begin(v),end(v),Modulo_print{os,m});
}
그에 비하면 람다를 사용한 버전이 확실히 낫다. 이름을 원한다면 람다의 이름을 지정할 수 있다.
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
for_each(begin(v),end(v),Modulo_print);
}
람다의 이름을 지정하는 것은 종종 좋은 생각이다. 그렇게 하면 작업 설계를 좀 더 신중하게 고려해야 한다. 또한 코드 레이아웃을 단순화하고 재귀를 허용한다.
for 루프를 작성하는 것은 for_each()와 함께 람다를 사용하는 것의 대안이다.
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for (auto x : v)
if (x%m==0) os << x << '\n';
}
많은 사람들이 이 버전이 람다 버전보다 훨씬 더 명확하다는 것을 알게 될 것이다. 그러나 for_each는 다소 특별한 알고리즘이며 vector <int>는 매우 구체적인 컨테이너다. 임의의 컨테이너를 처리하기 위해 print_modulo()를 일반화하는 것을 고려해 보자.
template<class C>
void print_modulo(const C& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for (auto x : v)
if (x%m==0) os << x << '\n';
}
이런 경우 map이 적합하다. ranged-for문은 시퀀스의 처음부터 끝까지 순회하는 특별한 경우에 특히 적합하다. STL 컨테이너는 이러한 순회를 쉽고 일반적으로 만든다.
template<class C>
void print_modulo(const C& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
breadth_first(begin(v),end(v),
[&os,m](int x) { if (x%m==0) os << x << '\n'; }
);
}
따라서 람다는 알고리즘으로 표현되는 일반화된 루프/순회 구성의 '본문'으로 사용할 수 있다. breadth_first 대신 for_each를 사용하면 depth-first 탐색을 제공한다.
순회 알고리즘에 대한 인수로서 람다의 성능은 등가 루프의 성능과 동일하다. (일반적으로) 구현 및 플랫폼 전반에 걸쳐 상당히 일관성이 있음을 알았다. 이 의미는 '알고리즘과 람다를 더한 것'과 'for문' 사이에서 선택해야 한다는 것이다.
캡처
람다의 주요 용도는 인수로 전달할 코드를 지정하는 것이다. 람다를 사용하면 함수(또는 함수 객체)의 이름을 지정하고 다른 곳에서 사용할 필요 없이 인라인으로 수행할 수 있다. 일반 람다는 로컬 환경에 대한 액세스가 필요하지 않는다. 이러한 람다는 empty lambda introducer []로 정의된다.
void algo(vector<int>& v)
{
sort(v.begin(),v.end()); // sor t values
// ...
sort(v.begin(),v.end(),[](int x, int y) { return abs(x)<abs(y); }); // sor t absolute values
// ...
}
로컬 이름에 액세스 하려면 그렇게 말해야 하며 그렇지 않으면 오류가 발생한다.
void f(vector<int>& v)
{
bool sensitive = true;
// ...
sort(v.begin(),v.end(),
[](int x, int y) { return sensitive ? x<y : abs(x)<abs(y); } // error : can’t access sensitive
);
}
lambda introducer []를 사용했다. 이것은 가장 간단한 lambda introducer이며 람다가 호출 환경에서 이름을 참조하는 것을 허용하지 않는다. 람다식의 첫 번째 문자는 항상 [. lambda introducer는 다양한 형태를 취할 수 있다.
- [] : empty capture list. 이는 주변 context의 로컬 이름을 람다 본문에 사용할 수 없음을 의미한다. 이러한 람다식의 경우 데이터는 인수 또는 비지역 변수에서 가져온다.
- [&] : 참조로 암시적으로 캡처한다. 모든 지역 이름을 사용할 수 있다. 모든 지역 변수는 참조로 액세스 된다.
- [=] : 값을 암시적으로 캡처한다. 모든 지역 이름을 사용할 수 있다. 모든 이름은 람다식 호출 시점에 가져온 지역 변수의 복사본을 참조한다.
- [capture-list] : 명시적 캡처; capture-list는 참조 또는 값으로 캡처할 지역 변수의 이름 목록이다. &가 앞에 오는 이름을 가진 변수는 참조로 캡처된다. 다른 변수는 값으로 캡처된다. 캡처 목록에는 이것과 이름 뒤에... 가 요소로 포함될 수도 있다.
- [&, capture-list] : 목록에 언급되지 않은 이름을 가진 모든 로컬 변수를 참조로 암시적으로 캡처한다. 캡처 목록에는 이것을 포함할 수 있다. 나열된 이름 앞에 &가 올 수 없다. 캡처 목록에 명명된 변수는 값으로 캡처된다.
- [=, capture-list] : 목록에 언급되지 않은 이름을 가진 모든 모든 지역 변수를 값으로 암시적으로 캡처한다. 캡처 목록에는 이것을 포함할 수 없다. 나열된 이름 앞에 &가 있어야 한다. 캡처 목록에 명명된 변수는 참조로 캡처된다.
&가 앞에 오는 지역 이름은 항상 참조로 캡처되고 &가 앞에 있지 않은 지역 이름은 항상 값으로 캡처된다. 참조에 의한 캡처만 호출 환경에서 변수 수정을 허용한다.
캡처 목록 사례는 호출 환경의 이름과 사용 방법을 세밀하게 제어하는 데 사용된다.
void f(vector<int>& v)
{
bool sensitive = true;
// ...
sort(v.begin(),v.end()
[sensitive](int x, int y) { return sensitive ? x<y : abs(x)<abs(y); }
);
}
캡처 목록에서 sensitive 항목을 언급하여 람다 내에서 액세스 할 수 있도록 한다. 달리 지정하지 않음으로써 sensitive 캡처가 값에 따라 수행되도록 한다. 인수 전달과 마찬가지로 사본 전달이 기본 값이다. sensitive를 참조로 캡처하고 싶다면 캡처 목록에서 sensitive 항목 앞에 &를 추가하면 된다. [&sensitive]
값에 의한 캡처와 참조에 의한 캡처 간의 선택은 기본적으로 함수 인수에 대한 선택과 동일하다. 캡처된 객체에 기록해야 하거나 객체가 큰 경우 참조를 사용한다. 그러나 람다의 경우 람다가 호출자보다 오래 지속될 수 있다는 추가적인 우려가 있다. 람다를 다른 thread에 전달할 때 일반적으로 값([=])으로 캡처하는 것이 가장 좋다. : 참조 또는 포인터를 통해 다른 thread의 스택에 액세스 하는 것은 성능이나 정확성에 가장 방해가 될 수 있으며 종료된 thread의 스택에 액세스 하려고 하면 찾기가 매우 어려운 오류가 발생할 수 있다.
가변 템플릿 인수를 캡처해야 하는 경우... 를 사용하자.
template<typename... Var>
void algo(int s, Var... v)
{
auto helper = [&s,&v...] { return s∗(h1(v...)+h2(v...)); }
// ...
}
람다 수명
람다는 호출자보다 오래 유지된다. 이것은 람다를 다른 thread에 전달하거나 호출 수신자가 나중에 사용하기 위해 람다를 저장하는 경우 발생한다.
void setup(Menu& m)
{
// ...
Point p1, p2, p3;
// compute positions of p1, p2, and p3
m.add("draw triangle",[&]{ m.draw(p1,p2,p3); }); // probable disaster
// ...
}
add()가 메뉴에 (name, action) 쌍을 추가하는 작업이고 draw() 작업이 의미가 있다면 이것은 폭탄과 다름없다. setup()이 완료되고 몇 분 후에 사용자가 삼각형 그리기 버튼을 누르고 람다가 사라진 지역 변수에 액세스 하려고 시도한다. 참조로 잡힌 변수에 쓰는 람다는 그 상황에서 훨씬 더 나쁘다.
람다가 호출자보다 오래 지속될 수 있는 경우 모든 로컬 정보가 클로저 객체에 복사되고 값이 반환 메커니즘 또는 적절한 인수를 통해 반환되는지 확인해야 한다.
m.add("draw triangle",[=]{ m.draw(p1,p2,p3); });
캡처 목록을 클로저 객체에 대한 초기화 목록으로 생각하고 [=] 및 [&]를 약식 표기법으로 생각하자.
네임스페이스 이름
네임스페이스 변수(전역 변수 포함)는 항상 액세스 할 수 있기 때문에 '캡처'할 필요가 없다.
template<typename U, typename V>
ostream& operator<<(ostream& os, const pair<U,V>& p)
{
return os << '{' << p.first << ',' << p.second << '}';
}
void print_all(const map<string,int>& m, const string& label)
{
cout << label << ":\n{\n";
for_each(m.begin(),m.end(),
[](const pair<string,int>& p) { cout << p << '\n'; }
);
cout << "}\n";
}
여기서 pair를 위해 cout 또는 출력 연산자를 캡처할 필요는 없다.
람다와 this
멤버 함수에 사용된 람다에서 클래스 객체의 멤버에 어떻게 액세스 할까? 캡처 목록에 추가하여 잠재적으로 캡처된 이름 집합에 클래스 멤버를 포함할 수 있다. 이것은 멤버 함수의 구현에서 람다를 사용하려는 경우에 사용된다. 예를 들어 요청을 작성하고 결과를 검색하는 클래스가 있을 수 있다.
class Request {
function<map<string,string>(const map<string,string>&)> oper; // operation
map<string,string> values; // arguments
map<string,string> results; // targets
public:
Request(const string& s); // parse and store request
void execute()
{
[this]() { results=oper(values); } // do oper to values yielding results
}
};
멤버는 항상 참조로 캡처된다. 즉, [this]는 구성원이 람다에 복사되지 않고 이를 통해 액세스 됨을 의미한다. 불행히도 [this]와 [=]는 호환되지 않는다. 이는 부주의한 사용이 multi-thread 프로그램에서 race condition으로 이어질 수 있음을 의미한다.
mutable 람다
일반적으로 우리는 함수 객체(클로저)의 상태를 수정하고 싶지 않으므로 기본적으로 그렇게 할 수 없다. 즉, 생성된 함수 객체에 대한 operator()()는 const 멤버 함수다. 드물지만 상태를 수정하려는 경우(참조로 캡처된 일부 변수의 상태를 수정하는 것과 반대로) 람다를 변경 가능으로 선언할 수 있다.
void algo(vector<int>& v)
{
int count = v.siz e();
std::generate(v.begin(),v.end(),
[count]()mutable{ return −−count; }
);
}
--count는 클로저에 저장된 v 크기의 복사본을 감소시킨다.
Call and Return
람다에 인수를 전달하는 규칙은 함수와 동일하고 결과 반환 규칙도 마찬가지다. 사실, 캡처 규칙을 제외하고 람다에 대한 대부분의 규칙은 함수 및 클래스에 대한 규칙에서 차용된다. 그러나 두 가지 불규칙성에 유의해야 한다.
- 람다식에 인수가 없으면 인수 목록을 생략할 수 있다. 따라서 최소 람다식은 []{}이다.
- 람다식의 반환 유형은 본문에서 추론할 수 있다. 불행히도, 그것은 또한 함수에 대해 수행되지 않는다.
람다 본문에 return문이 없으면 람다의 반환 유형은 void다. 람다 본문이 단일 return문으로 구성된 경우 람다의 반환 유형은 return 표현식의 유형이다. 둘 다 그렇지 않은 경우 반환 유형을 명시적으로 제공해야 한다.
void g(double y)
{
[&]{ f(y); } // return type is void
auto z1 = [=](int x){ return x+y; } // return type is double
auto z2 = [=,y]{ if (y) return 1; else return 2; } // error : body too complicated
// for retur n type deduction
auto z3 =[y]() { return 1 : 2; } // return type is int
auto z4 = [=,y]()−>int { if (y) return 1; else return 2; } // OK: explicit return type
}
접미사 반환 유형 표기법을 사용할 때 인수 목록을 생략할 수 없다.
람다의 타입
람다식의 최적화된 버전을 허용하기 위해 람다식의 유형은 정의되지 않는다. 단, 클로저 유형으로 정의한다. 이 유형은 람다에 고유하므로 두 개의 람다가 동일한 유형을 갖지 않는다. 두 개의 람다가 같은 유형을 가졌다면 템플릿 인스턴스화 메커니즘이 혼동되었을 수 있다. 람다는 생성자와 const 멤버 함수 operator()()가 있는 로컬 클래스 유형이다. 람다를 인수로 사용하는 것 외에도 auto 또는 std::function<R(AL)>로 선언된 변수를 초기화하는 데 사용할 수 있다. 여기서 R은 람다의 반환 유형이고 AL은 유형의 인수 목록이다.
예를 들어 람다를 작성하여 C 스타일 문자열의 문자를 뒤집을 수 있다.
auto rev = [&rev](char∗ b, char∗ e)
{ if (1<e−b) { swap(∗b,∗−−e); rev(++b,e); } }; // error
그러나 유형이 추론되기 전에는 auto 변수를 사용할 수 없기 때문에 불가능하다. 대신 이름을 소개한 다음 사용할 수 있다.
void f(string& s1, string& s2)
{
function<void(char∗ b, char∗ e)> rev =
[&](char∗ b, char∗ e) { if (1<e−b) { swap(∗b,∗−−e); rev(++b,e); } };
rev(&s1[0],&s1[0]+s1.siz e());
rev(&s2[0],&s2[0]+s2.siz e());
}
이제는 rev 유형이 사용되기 전에 지정된다.
재귀적으로 사용하지 않고 람다의 이름을 지정하려는 경우 auto는 일을 단순화할 수 있다.
void g(vector<string>& vs1, vector<string>& vs2)
{
auto rev = [&](char∗ b, char∗ e) { while (1<e−b) swap(∗b++,∗−−e); };
rev(&s1[0],&s1[0]+s1.siz e());
rev(&s2[0],&s2[0]+s2.siz e());
}
아무것도 캡처하지 않는 람다는 적절한 유형의 함수에 대한 포인터를 할당할 수 있다.
double (∗p1)(double) = [](double a) { return sqrt(a); };
double (∗p2)(double) = [&](double a) { return sqrt(a); }; // error : the lambda captures
double (∗p3)(int) = [](int a) { return sqrt(a); }; // error : argument types do not match
'Program Language > C & C++' 카테고리의 다른 글
[C++] 함수의 지정자와 수정자 (0) | 2022.01.13 |
---|---|
[C++] 명시적 유형 변환 (0) | 2022.01.12 |
[C++] Constant Expressions (0) | 2022.01.06 |
[C++] token 요약 (0) | 2022.01.06 |
[C++] operator 요약 (0) | 2022.01.06 |