람다가 없을 때에는 STL의 "_if" 알고리즘들(std::find_if, std::remove_if, std::count_if 등)이 가장 자명한 술어(predicate)들 하고만 함께 쓰이는 경향이 있다
하지만 람다가 있으면 그런 알고리즘들을 자명하지 않은 조건들과 함께 사용하는 사례가 폭발적으로 늘어난다. 비교 함수로 커스텀화할 수 있는 알고리즘들(std::sort, std::nth_element, std::lower_bound 등)에 대해서도 이다.
람다는 STL 이외의 표준 라이브러리 구성요소들에서도 유용하게 쓰인다
- std::unique_ptr, std::shared_ptr를 위한 커스텀 삭제자를 간단하게 만들어낼 수 있고, 다중 스레드 API의 조건 변수를 위한 술어를 지정하는 것도 간단하다
람다는 콜백 함수나 인터페이스 적용(adaption) 함수, 단발성(일회성) 호출을 위한 문맥 국한적 함수를 즉석에서 지정할 때 아주 유용하다
람다 표현식(lamda expression) : 하나의 표현식으로, 소스 코드의 일부이다.
std::find_if(container.begin(), container.end(), [](int val){return 0<val && val<10;});
클로저(closure) : 람다에 의해 만들어지는 실행시점 객체, 캡처([])에 따라, 클로저가 캡처된 자료의 복사본을 가질 수도 있고 그 자료에 대한 참조를 가질 수 있다
클로저 클래스 : 클로저를 만드는데 쓰인 클래스, 각각의 람다에 대해 컴파일러는 고유한 클로저 클래스를 만들어낸다. 람다 안의 문장들은 해당 클로저 클래스의 멤버 함수들 안의 실행 가능한 명령들이 된다.
컴파일 시점에서 존재하는 것(람다와 클로저 클래스), 실행시점에서 존재하는 것(클로저)
{
int x; // x는 지역 변수
...
auto c1 = [x](int y){return x*y>55;}; // c1은 람다에 의해 만들어진 클로저의 복사본
auto c2 = c1; // c2는 c1의 복사본
auto c3 = c2; // c3는 c2의 복사본
...
}
항목31: 기본 갈무리 모드를 피하라 (여기서 갈무리는 캡처이다)
C++11의 기본 갈무리 모드(default capture mode)는 두 가지로 나누어진다
- 참조에 의한(by-reference) 갈무리 모드(참조 갈무리 모드)
- 값에 의한(by-value) 갈무리 모드(값 갈무리 모드)
기본 갈무리 모드는 참조가 대상을 잃을(dangling)위험이 있다
기본 갈무리 모드는 자기 완결적(self-contained)일 것 같지만, 그렇지 않은 경우도 있다.
참조 캡처를 사용하는 클로저는 지역 변수 또는 람다가 정의된 범위에서 볼 수 있는 매개변수에 대한 참조를 가지게 된다.
람다에 의해 생성된 클로저의 수명이 그 지역 변수나 매개변수의 수명보다 오래 지속되면, 클로저 안의 참조는 대상을 잃는다
void addDivisorFilter()
{
auto calc1 = computerSomeValue1();
auto calc2 = computerSomeValue2();
auto divisor = computerDivisor(calc1, calc2);
filters.emplace_back([&](int value) {return value%divisor == 0;}); // divisor에 대한 참조가 대상을 잃을 수 있음
}
/*
1. divisor의 시한부 인생 (지역 변수)
코드에서 auto divisor는 addDivisorFilter() 함수 안에서 만들어진 지역 변수(Local Variable)입니다. C++에서 지역 변수는 스택(Stack) 메모리에 살다가, 함수가 끝나는 닫는 괄호 }를 만나는 순간 즉시 파괴되어 사라집니다.
그래서 필터는 거의 생성 직후부터 미정의 행동을 유발할 수 있다.
2. 람다의 착각: "주소만 적어갈게!" ([&])
람다를 만들 때 [&] (참조 갈무리)를 사용했습니다. 이 말은 divisor 안에 들어있는 진짜 숫자(예: 5)를 복사해서 챙긴 게 아니라, divisor가 살고 있는 메모리 주소표(포인터)만 달랑 챙겨왔다는 뜻입니다.
3. 비극의 시작: 함수는 끝났고, 람다는 남았다
이 코드가 가장 치명적인 이유는 만들어진 람다를 filters.emplace_back(...)을 통해 함수 바깥에 있는 리스트에 저장(탈출)시켰기 때문입니다.
함수 종료: addDivisorFilter() 함수가 끝납니다. 스택이 정리되면서 divisor 변수는 메모리에서 영원히 파괴됩니다.
나중에 람다가 실행될 때: 게임이 돌아가다가 나중에 누군가 filters 리스트에서 이 람다를 꺼내서 실행(lambda(10))합니다.
대참사 (대상을 잃음): 람다는 "아! 아까 적어둔 주소로 가서 divisor 값을 읽어와야지!" 하고 메모리에 접근합니다. 하지만 그 주소에 있던 집(divisor)은 이미 철거되었고, 엉뚱한 쓰레기 데이터만 남아있습니다. 프로그램은 영문도 모른 채 펑 터지게 됩니다.
*/
filters.emplace_back([&divisor](int value) {return value%divisor == 0;}); // divisor에 대한 참조가 대상을 잃을 수 있음
위의 코드에서 장점은 람다 표현식의 유효성이 divisor의 수명에 의존한다는 점이 명확히 나타낸다
divisor가 적어도 람다의 클로저만큼은 살아 있어야 한다는 점을 깨닫게 될 가능성이 크다
[&]가 암시하는 그 어떤 참조도 대상을 잃지 않게 해야한다라는 일반적인 경고보다 좀 더 구체적인 힌트이다.
template<typename C>
void workWithContainer(const C& container)
{
auto calc1 = computerSomeValue1();
auto calc2 = computerSomeValue2();
auto divisor = computerDivisor(calc1, calc2);
using ContElemT = typename C::value_type; // 컨테이너에 담긴 요소들의 형식
using std::begin; // 일반성을 위해
using std::end;
if(std::all_of(begin(container), end(container), // 컨테이너의 모든 값이 divisor의 배수인가?
[&](const ContElemT& value) {return value % divisor ==0;}))
{
...} // 그런 경우
else{
...} //아닌 경우
}
}
/*
1. 코드의 역할: 그 자리에서 즉시 실행됨
이 코드는 컨테이너(예: vector) 안에 있는 모든 값들이 divisor의 배수인지 검사하는 로직입니다. 핵심은 std::all_of 알고리즘 함수입니다.
방금 전의 filters.emplace_back()은 람다를 리스트에 '저장'해 두고 나중에 꺼내 쓰기 때문에 변수가 파괴될 위험이 있었습니다.
하지만 std::all_of는 람다를 넘겨받자마자 그 자리에서 즉시 루프를 돌며 '실행'을 해버립니다.
2. 왜 여기서는 [&] (참조 갈무리)가 안전할까?
이 코드에서 람다의 수명은 if 문이 끝나는 순간 같이 끝납니다.
즉, 지역 변수인 divisor가 파괴되기(함수가 끝나기) 훨씬 전에 람다가 먼저 임무를 다하고 사라지기 때문에, 참조 대상(divisor)을 잃어버리는 댕글링 참조 버그가 절대 발생하지 않습니다. (완벽하게 안전한 상태입니다!)
3. 책의 진짜 의도 (스콧 마이어스의 함정 카드)
독자들이 여기까지 읽고 "아하! STL 알고리즘처럼 그 자리에서 바로 실행되는 거면 [&]를 편하게 써도 되겠네!" 라고 생각할 때쯤, 저자는 이렇게 뒤통수를 칩니다.
"네, 지금 당장은 안전합니다. 하지만 그래도 [&]는 쓰지 마세요!"
왜냐하면 코드는 계속 변하기 때문입니다.
지금은 람다가 std::all_of 안에서만 안전하게 쓰이고 있습니다.
하지만 몇 달 뒤, 다른 동료(혹은 미래의 나)가 이 코드를 보고 "오, 이 람다 조건식 좋은데? 나중에 다른 곳에서도 재사용해야지!" 하고 이 람다를 복사해서 filters.emplace_back() 같은 곳에 집어넣어 버릴 수 있습니다.
그 순간, 안전했던 코드가 최악의 시한폭탄 버그로 돌변합니다.
*/
위 코드가 안전한 것은 사실이나, 그 안전성은 깨지기 쉽다. 이 람다가 다른 문맥에서도 유용할 것으로 생각해서 람다를 복사해서 그 문맥에 붙여넣을 수도 있다. 그런데 그 문맥에서 divisor가 클로저보다 먼저 소멸한다면, 앞에서 살펴본 참조가 대상을 잃는 문제가 발생하게 된다.
위와 같은 문제를 해결하기 위해서는 기본 값 갈무리 모드를 사용하는 것이다.
filters.emplace_back([=](int value) {return value%divisor==0;} // 이제는 divisor가 대상을 잃지 않음
);
기본 값 갈무리가 대상을 잃은 문제에 대한 특효약이 아니다. 왜냐하면 포인터를 값으로 갈무리하면 그 포인터는 람다에 의해 생성된 클로저 안으로 복사되는데, 람다 바깥의 어떤 코드가 그 포인터를 delete로 삭제하지 않는다는 보장은 없으며, 그런 일이 발생하면 포인터 복사본은 지칭 대상을 잃게 된다.
class Widget{
public:
... // 생성자 등
void addFilter() const; // 필터를 filters에 추가
private:
int divisor; // Widget의 필터에 쓰인다
};
void Widget::addFilter() const
{
filters.emplace_back([=](int value) {return value%divisor==0;}); // disvisor는 클래스의 한 자료 멤버이기 때문에 캡처가 안된다.
}
void Widget::addFilter() const
{
filters.emplace_back([](int value) {return value%divisor==0;}); // disvisor를 사용할 수 없음
}
void Widget::addFilter() const
{
filters.emplace_back([divisor](int value) {return value%divisor==0;}); // 캡처할 divisor가 없음
}
또한, 갈무리는 오직 람다가 생성된 범위에서 보이는, static이 아닌 지역 변수(매개변수 포함)에만 적용된다.
그렇게 생긴 이유
void Widget::addFilter() const
{
filters.emplace_back([=](int value) {return value%divisor==0;});
}
// 이 코드는 컴파일러가 마치 아래의 코드와 같다. divisor가 아니라 Widget의 this포인터이다.
void Widget::addFilter() const
{
auto currentObjectPtr=this;
filters.emplace_back([currentObjectPtr](int value) {return value%currentObjectPtr->divisor==0;});
}
-> 암묵적으로 this 포인터가 쓰이기 때문이다. 모든 비static 멤버 함수에는 this 포인터가 있으며, 클래스의 멤버 함수를 언급할 때마다 그 포인터가 쓰인다
-> 이 람다에서 만들어진 클로저의 유효성이 해당 Widget 객체(클로저가 가진 this 복사본의 원본에 해당하는)의 수명에 의해 제한된다
위 문제를 해결하기 위해서는 아래와 같이 코드를 짜면 된다
void Widget::addFilter() const
{
auto divisorCopy = divisor; // 자료 멤버를 복사한다.
filters.emplace_back(
[divisorCopy](int value) // 복사본을 캡처한다
{return value%divisorCopy==0;} // 복사본을 사용한다
);
}
void Widget::addFilter() const
{
auto divisorCopy = divisor; // 자료 멤버를 복사한다.
filters.emplace_back(
[=](int value) // 복사본을 캡처한다
{return value%divisorCopy==0;} // 복사본을 사용한다
);
}
또는, 일반화된 람다 갈무리를 사용하는 것이다.
void Widget::addFilter() const
{
filters.emplace_back( // C++14
[divisor=divisor](int value) // divisor를 클로저에 복사한다.
{return value%divisor==0;} // 복사본을 사용한다
);
}
값에 의한 기본 갈무리 모드의 또 다른 단점은, 해당 클로저가 자기 완결적이고 클로저 바깥에서 일어나는 자료의 변화로부터 격리되지 않는다.
람다가 지역 변수와 매개변수(캡처가 가능한)뿐만 아니라 정적 저장소 수명 기간(static storage duration)을 가진 객체에도 의존할 수 있기 때문이다. 하지만, 그런 객체를 람다 안에서 사용할 수는 있지만, 캡처할 수는 없다.
void myLogic() {
static int divisor = 5; // 정적 변수 (static)
int local_val = 10; // 지역 변수
// 개발자의 생각: "[=]를 썼으니까 divisor(5)와 local_val(10)이 완벽하게 복사되었겠지?"
auto myLambda = [=]() {
std::cout << "local: " << local_val << ", divisor: " << divisor << "\n";
};
// 바깥세상에서 값을 바꿔봄
local_val = 999;
divisor = 999;
myLambda(); // local: 10, divisor: 999
}
항목 32 : 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라
초기화 갈무리(init capture)
- C++14는 객체를 클로저 안으로 이동하는 수단을 직접 제공한다
- 초기화 캡처로는 C++11의 캡처들이 할 수 있는 모든 것을 할 수 있으며, 그 외의 여러 가지 것들도 할 수 있다.
- 람다로부터 생성되는 클로저 클래스에 속한 자료 멤버의 이름, 그 자료 멤버를 초기화하는 표현식을 지정할 수 있다
class Widget{ // 어떤 유용한 형식
public:
...
bool isValidated() const;
bool isProcessed() const;
bool isArchived() const;
private:
...
};
auto pw = std::make_unique<Widget>(); // Widget을 생성한다.
...
auto func = [pw = std::move(pw)] {return pw->isValidated() && pw->isArchived();}; // 클로저의 자료 멤버를 std::move(pw)로 초기화한다
pw=std::move(pw)는 초기화 캡처이다. "="의 좌변은 클로저 클래스 안의 자료 멤버(클로저에서 사용할)의 이름이고, 우변은 그것을 초기화하는 표현식이다.
좌변의 범위는 해당 클로저 클래스의 범위이고, 우변의 범위는 람다가 정의되는 지점의 범위와 동일하다
즉, "pw=std::move(pw)"는 클로저 안에서 자료 멤버 pw를 생성하되, 지역 변수 pw에 std::move를 적용한 결과로 그 자료 멤버를 초기화하라는 뜻이다.
auto func = [pw = std::make_unique<Widget>()] {return pw->isValidated() && pw->isArchived();};
// 클로저 안의 자료 멤버를 make_unique 호출 결과로 초기화
/*
"코드에 굳이 auto pw = std::make_unique... 라고 지역 변수를 따로 선언했다는 것은, 람다 대괄호 안에 집어넣기 전에 그 객체의 값을 요리조리 세팅할 일이 있다는 뜻이다.
만약 그런 세팅 작업이 전혀 필요 없다면, 뭐 하러 귀찮게 지역 변수를 따로 만들고 그걸 또 move로 옮기나? 그냥 람다 대괄호 [] 안에서 std::make_unique를 직접 호출해서 한 방에 생성과 갈무리를 끝내버려라!"
*/
C++11에서는 어떤 표현식의 결과를 캡처하는 것이 불가능했지만, C++14에서는 가능하다. 즉, C++11에 비해 일반화가 되기 때문에 일반화된 람다 갈무리(generalized lambda capture)라고 부른다.
그리고 람다 표현식은 그냥 컴파일러가 하나의 클래스를 자동으로 작성해서 그 클래스의 객체를 생성하게 만드는 수단일 뿐이다.
C++11에서 이동 캡처를 흉내내는 방법
- 캡처할 객체를 std::bind가 산출하는 함수 객체로 이동하고,
- 그 '캡처된' 객체에 대한 참조를 람다에 넘겨준다.
//C++14
std::vector<double> data; // 클로저 안으로 이동할 객체 (이동할 객체의 형식)
... // data에 자료를 채운다 (객체의 이름)
auto func = [data = std::move(data)] {/*data 사용*/}; // C++14의 초기화 캡처 (초기화 캡처를 위한 초기화 표현식)
//C++11
std::vector<double> data; // 클로저 안으로 이동할 객체
... // data에 자료를 채운다
auto func = std::bind([](const std::vector<double>& data)
{/*data 사용*/}, std::move(data)); // C++11에서 초기화 캡처를 흉내내는 방법
람다 표현식처럼 std::bind는 함수 객체를 산출한다. 이 std::bind가 돌려주는 함수 객체를 바인드 객체라고 한다
std::bind의 첫 인수는 호출 가능한 객체이고, 나머지 인수들은 그 객체에 전달할 값들을 나타낸다.
바인드 객체는 std::bind에 전달된 모든 인수의 복사본들을 포함한다.
각 왼값 인수에 대해, 바인드 객체에는 그에 해당하는 복사 생성된 객체가 있다.
각 오른값에 대해서는 이동 생성된 객체가 있다.
-> data는 바인드 객체 안으로 이동된다. 이 이동 생성이 바로 이동 객체 흉내의 핵심이다
-> 오른값을 바인드 객체 안으로 이동함으로써 오른값의 이동이 불가능하다는 C++11 클로저의 한계를 우회한다.
이 람다는 C++14 버전에서 사용한 람다와 같되, 이동 캡처 흉내용 객체에 해당하는 data 매개변수가 추가되었다는 차이가 있다.
이 매개변수는 바인드 객체 안의 data 복사본에 대한 왼값 참조이다. (data의 복사본을 초기화하는 데 쓰인 표현식("std::move(data)")은 오른값이지만, data의 복사본 자체는 왼값이다.)
-> 람다 본문 안에서 data를 사용하는 코드는 바인드 객체 안의 이동 생성된 data복사본을 사용하게 된다.
기본적으로, 람다로부터 만들어진 클로저 클래스의 operator()멤버 함수는 const이다. 이 때문에 람다 본문 안에서 클로저의 모든 자료 멤버는 const가 된다. 그러나 바인드 객체 안의 이동 생성된 data 복사본은 const가 아니다. 그래서 const 키워드를 붙여야 한다.
하지만 변이 가능한 람다를 사용한다면, 람다를 mutable로 선언하면, 해당 클로저 클래스의 operator()는 const로 선언되지 않을 것이므로 람다의 매개변수 선언에서 const를 제거해야 마땅하다
바인드 객체는 std::bind에 전달된 모든 인수의 복사본을 저장하므로, 이 예의 바인드 객체는 람다가 산출한 클로저(std::bind의 첫 인수)의 복사본도 저장한다. 즉, 클로저의 수명은 바인드 객체의 수명과 같다.
객체를 C++11 클로저 안으로 이동 생성하는 것은 불가능하나, 객체를 C++11 바인드 객체 안으로 이동 생성하는 것은 가능하다
C++11에서 이동 갈무리를 흉내 내는 방법은, 객체를 바인드 객체 안으로 이동 생성하고, 이동 생성된 객체를 람다에 참조로 전달하는 것
바인드 객체의 수명이 클로저의 수명과 같으므로, 바인드 객체 안의 객체들을 마치 클로저 안에 있는 것처럼 취급하는 것이 가능하다.
항목 33 : std::forward를 통해서 전달할 auto&& 매개변수에는 decltype를 사용하라
C++14에서 가장 고무적인 기능은 일반적 람다(generic lambdas), 즉 매개변수 명세에 auto를 사용하는 람다이다
auto f = [](auto x){return normalize(x);}
// 실제로 컴파일하면 아래와 같다
// (즉, 이 람다가 산출하는 클로저 클래스(컴파일러가 대신 만들어주는 클래스 설계도)의 함수 호출 연산자)
class 컴파일러가_만든_어떤_클래스_이름{
public:
template<typename T>
auto operator()(T x) const
{return normalize(x);}
...
};
위의 람다는 normalize에 항상 왼값(매개변수 x)를 전달하기 때문에 왼값과 오른값을 다른 방식으로 처리해야한다
x를 완벽히 전달하려면 아래와 같은 조건을 만족해야 한다
- x가 보편 참조이어야 한다
- std::forward를 통해서 normalize에 전달해야 한다
auto f = [](auto&& x) {return normalize(std::forward<???>(x);};
???에 템플릿 변수를 넣으면 좋겠지만 auto로 인하여 operator() 클로저 클래스의 템플릿 변수를 넣자니 개발자는 모르기 때문에 템플릿을 사용할 수 없다.
이를 해결하기 위해서 이 람다에서 주어진 인수가 왼값인지 오른값인지는 매개변수 x를 조사하면 되는데 그 수단이 decltype이다.
- 왼값이 전달되면 왼값참조 형식 산출하고 오른값이면 오른값 참조 형식을 산출한다
- foward의 <>부분에 비참조로 인하여 관례에 맞지 않은 것처럼 보이지만 참조 축약으로 인하여 괜찮다
Widget&& && forward(Widget& param)
{
return static_cast<Widget&& &&>(param);
// T가 Widget&&일 때의 std::forward의 인스턴스화 결과 (참조 축약 적용X)
}
Widget&& forward(Widget& param)
{
return static_cast<Widget&&>(param);
// T가 Widget&&일 때의 std::forward의 인스턴스화 결과 (참조 축약 적용)
}
즉, 왼값이든 오른값이든 decltype(x)를 std::forward로 넘겨주면 원하는 결과가 나온다.
auto f = [](auto&& x) {return normalize(std::forward<decltype(x)>(x));};
아래의 예에서 마침표 세 개를 두 번 추가해주면 매개변수 하나가 아니라 임의의 개수의 매개변수들을 받아서 완벽하게 전달하는 람다가 된다
- 이는 C++14의 람다가 가변 인수(몇 개 들어올지 모르는 미정의 인수)를 지원하는 덕분이다.
auto f = [](auto&&... x) {return normalize(std::forward<decltype(xs)>(xs)...);};
항목 34 : std::bind보다 람다를 선호하라
std::bind가 돌려준 함수 객체를 바인드 객체라고 한다
std::bind보다 람다를 선호할 가장 중요한 이유는 람다가 가독성이 더 좋다
가독성
using Time=std::chrono::steady_clock::time_point;
enum class Sound {Beep, Siren, Whistle};
using Duration = std::chrono::steady_clock::duration;
void SetAlarm(Time t, Sound s, Duration d);
auto setSoundL = [](Sound s)
{
using namespace std::chrono;
setAlarm(steady_clock::now()+hours(1), s, seconds(30));
}; // 한 시간 후부터 지정된 경보음을 30초 간 재생
// C++ 14버전
auto setSoundL = [](Sound s)
{
using namespace std::chrono;
using namespace std::literals;
setAlarm(steady_clock::now()+1h, s, 30s);
};
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
auto setSoundB =
std::bind(setAlarm, steady_clock::now()+1h, _1, 30s);
2번째 코드는 람다로 구현한 것으로 setAlarm에 대한, 소리만 지정하면 되는 인터페이스를 람다를 이용해서 작성이다. 'steady_clock::now()+1h'라는 표현식이 setAlarm에 전달되는 하나의 인수임이 명확하여 setAlarm을 호출한 시점에서부터 한 시간이 지난 후에 경보고 울린다
std::bind를 사용하면 _1 자리에 나중에 전달한 인수가 들어가므로, setSoundB(sound)의 sound는 setAlarm의 두 번째 인수로 전달된다. 그러나 steady_clock::now() + 1h는 setAlarm 호출 시점에 계산되는 것이 아니라, std::bind가 실행되는 순간 먼저 계산되어 바인드 객체 안에 저장된다. 따라서 setSoundB를 나중에 호출하더라도 알람 시간은 호출 시점 기준 1시간 뒤가 아니라, 바인드 객체를 만든 시점 기준 1시간 뒤가 된다.
위의 문제를 해결하기 위해서는 그 표현식을 setAlarm 호출 때까지 지연하라고 std::bind에게 알려줘야 한다. 즉, 첫 std::bind 안에 두 개의 함수 호출을 내표시켜야 한다.
// C++14에서는 표준 연산자 템플릿에 대한 템플릿 형식 인수를 생략할 수 있음
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<>(), std::bind(steady_clock::now),1h)
, _1, 30s);
// C++11 이전 버전의 형식
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(), std::bind(steady_clock::now),1h)
, _1, 30s);
오버로딩
enum class Volume{Normal, Loud, LoudPlusPlus};
void setAlarm(Time t, Sound s, Duration d, Volume v);
람다는 오버로딩 해소에 의해 setAlarm의 인수 세 개짜리 버전을 선택한다
하지만, bind는 컴파일 되지 않는다. 왜냐하면 컴파일로서는 두 setAlarm 함수 중 어떤 것을 std::bind에 넘겨줘어야할지 결정할 방법이 없기 때문이다
- 해결하기 위해서는 setAlarm을 적절한 함수 포인터 형식으로 캐스팅해야 한다
// 매개변수 3개짜리 함수 포인터 타입을 정의하고...
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
std::bind(std::plus<steady_clock::time_point>(), std::bind(steady_clock::now),1h)
, _1, 30s);
위의 차이점 뿐만 아니라 setSoundL에 대한 함수 호출자 연산자(즉, 람다의 클로저 클래스의 함수 호출 연산자) 안에서 setAlarm 호출은 컴파일러가 통상적인 방식으로 인라인화(inlining)할 수 있는 보통의 함수 호출이다
setSoundL(Sound::Siren); // setAlarm의 본문의 인라인화될 가능성이 크다
std::bind 호출은 setAlarm을 가리키는 함수 포인터를 전달하므로, setSoundB에 대한 함수 호출 연산자(즉, 바인드 객체에 대한 함수 호출 연산자) 안에서 setAlarm 호출은 함수 포인터를 통해서 일어난다.
함수 포인터를 통한 함수 호출은 컴파일러가 인라인화할 가능성이 더 낮다. 따라서 setSoundB를 통한 setAlarm 호출은 setSoundL을 통한 호출에 비해 완전히 인라인화될 가능성이 작다
- 즉, std::bind를 사용할 때보다 람다를 사용할 때 더 빠른 코드가 산출될 수 있다.
std::bind는 값으로 전달된다. 이를 알기 위해서는 반드시 std::bind의 작동 방식을 알고 있어야 한다는 것이다.
반면, 람다 접근 방식에서는 값을 캡처할지 참조로 캡처할지에 따라서 소스 코드에 명백히 드러난다
- 매개변수들이 전달되는 방식도 캡처만큼이나 명백하게 드러난다
- bind도 이 부분에서는 명확하지 않고 bind의 작동 방식을 알아야 한다. (그래서 바인드 객체에 전달되는 모든 인수는 참조로 전달된다. 이는 객체의 함수 호출 연산자가 완벽전달을 사용하기 때문이다.)
따라서, std::bind를 사용하는 코드는 람다에 비해 읽기 힘들고 표현력이 낮다. 그리고 효율성도 떨어질 가능성이 있다.
C++11에서는 std::bind를 사용하는 경우가 있다
1. 이동 캡처(move capture)
- C++11 람다는 이동 캡처를 지원하지 않는다
- 람다와 std::bind의 조합을 통해서 흉내내는 것은 가능하다(항목32)
- C++14에서는 초기화 캡처로 bind가 필요없다
2. 다형적 함수 객체(polymorphic function object)
- 바인드 객체에 대한 함수 호출 연산자는 완벽 전달을 사용하기 때문에 그 어떤 형식의 인수들도 받을 수 있다
- 이는 객체를 템플릿화된 함수 호출 연산자와 묶으려 할 때 유용하다
class PolyWidget{
public:
template<typename T>
void operator()(const T& param) const;
...
};
- C++14에서 auto 매개변수를 가진 람다로 간단히 구현이 가능하다
auto boundPW = [pw](const auto& param) {pw(param);};
'CS > C++' 카테고리의 다른 글
| Effective Modern C++ 3장 : 현대적인 C++에 적응하기 (1) (0) | 2026.07.07 |
|---|---|
| Effective Modern C++ 2장 : auto (0) | 2026.06.29 |
| Effective Modern C++ 1장 : 형식 연역 (0) | 2026.06.23 |
| Effective Modern C++ 5장 : 오른값 참조, 이동 의미론, 완벽 전달 (0) | 2026.02.13 |
| Effective Modern C++ 4장 : 똑똑한 포인터 (0) | 2025.12.13 |