KoreanFoodie's Study
Effective Modern C++ | 항목 34 : std::bind 보다 람다를 선호하라 본문
Effective Modern C++ | 항목 34 : std::bind 보다 람다를 선호하라
GoldGiver 2022. 10. 26. 10:05
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 34 : std::bind 보다 람다를 선호하라
핵심 :
1. std::bind 를 사용하는 것보다 람다가 더 읽기 쉽고 표현력이 좋다. 그리고 더 효율적이다.
2. C++14 가 아닌 C++11 에서는 이동 갈무리를 구현하거나 객체를 템플릿화된 함수 호출 연산자에 묶으려 할 때 std::bind 가 유용할 수 있다.
C++11 에서는 거의 항상, C++14 에서는 확고하게 람다가 std::bind 에 비해 우월한 선택이다. 다음과 같은 예시를 보자.
// 시간상의 한 지점을 대표하는 형식 별칭
using Time = std::chrono::steady_clock::time_point;
// enum class 에 관해서는 항목 10을 조라
enum class Sound { Beep, Siren, Whistle };
// 시간 길이를 나타내는 형식에 대한 별칭 선언
using Duration = std::chrono::steady_clock::duration;
// 시간 t 에서 소리 s 를 기간 d 만큼 출력한다
void setAlarm(Time t, Sound s, Duration d);
// 경보음을 직접 지정해서 한 시간 후부터 30초간 울리게 하는 함수 객체
auto setSoundL =
[](Sound s)
{
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1),
s,
seconds(30));
};
C++14 버전에서는 std::literals 기능을 이용해 setSoundL 을 조금 간결하게 만들 수 있다.
// 경보음을 직접 지정해서 한 시간 후부터 30초간 울리게 하는 함수 객체
auto setSoundL =
[](Sound s)
{
using namespace std::chrono;
using namespace std::literals;
setAlarm(steady_clock::now() + 1h,
s,
30s);
};
위 함수를 std::bind 를 사용해 작성한 코드는 다음과 같다.
using namespace std::chrono;
using namespace std::literals;
// _1 을 사용하는 데 필요함
using namespace std::placeholders;
auto setSoundB =
std::bind(setAlarm,
steady_clock::now + 1h,
_1,
30s);
위에서 setSoundB 를 호출하면 std::bind 호출에 지정된 시간과 기간으로 setAlarm 이 호출된다. 그리고 자리표(placeholder) 를 통해 setSoundB 부분의 첫 인수가setAlarm 의 둘째 인수로 전달된다는 것을 파악하려면, 다시 setAlarm 의 선언을 들여다 보아야 한다.
그런데 위 코드는 문제가 있다. std::bind 호출에서 인수 "steady_clock::now() + 1h" 는 setAlarm 이 아니라 std::bind 로 전달되며, 따라서 그 표현식이 평가되어 나오는 시간은 std::bind 가 생성한 바인드 객체에 저장된다. 결과적으로, 경보는 setAlarm 을 호출하고 한 시간이 지난 후가 아니라 std::bind 를 호출하고 한 시간이 지난 후에 울린다.
이 문제를 고치려면, 그 표현식을 setAlarm 호출 때까지 지연하라고 std::bind 에게 알려주어야 한다. 그러려면 첫 std::bind 안에 두 개의 함수 호출을 내포시켜야 한다.
// C++14 에서는 std::plus<> 로 수정 가능
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(),
std::bind(steady_clock::now),
1h),
_1,
30s);
이제 람다를 선호해야 하는 이유를 이해했을 것이다.
또한, std::bind 는 중복적재를 제대로 해결할 수 없다. 만약 위에서 인수 4 개를 받는 setAlarm 을 새로 선언했다고 하자.
void setAlarm(Time t, Sound s, Duration d, Volume v);
람다 버전은 인수가 3 개인 버전을 호출하지만, std::bind 버전은 컴파일되지 않는다. 컴파일러는 두 setAlarm 함수 중 어떤 것을 std::bind 에 넘겨줄지 알 수 없기 때문이다. 이를 해결하려면 setAlarm 을 함수 포인터 형식으로 캐스팅해야 한다.
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 은 통상적으로 인라인화할 수 있는 코드인 반면, std::bind 호출은 함수 포인터를 전달하는데 함수 포인터를 통한 함수 호출은 컴파일러가 인라인화 할 가능성이 더 낮다. 그러므로 람다가 더 빠른 코드를 산출할 가능성이 높다.
마지막으로, std::bind 는 전달되는 객체가 값으로 전달되는지 참조로 전달되는지 파악하기 어렵다. 반면, 람다는 캡쳐 리스트에서 '=' 혹은 '&' 을 통해 쉽게 파악할 수 있다(기본은 값으로 캡쳐).
enum class CompLevel { Low, Normal, High };
Widget compress(const Widget& w, CompLevel
lev);
Widget w;
using namespace placeholders;
auto compressRateB = std::bind(compress, w, _1);
compressRateB(CompLevel::High);
바인드 객체에 전달되는 모든 인수는 참조로 전달되므로, CompLevel::High 는 참조로 전달되며, compressRateB 에서의 w 는 값으로 전달된다.
이러한 여러가지 이유로, C++11 에서는 std::bind 가 비권장(deprecate) 기능이 되었으며, C++14 에서는 사용하는 것이 합당한 경우가 전혀 없다는 것을 잘 알아두자.