C++11에서는 람다가 거의 항상 std::bind보다 나은 선택이고 C++14에서는 거의 항상 수준이 아니라 무조건 좋은 선택이다. 게다가 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); // t시간 뒤에 소리 s를 d만큼 출력
일정 시간 뒤에 알람이 울리는 함수가 있다.
auto setSoundL = [](Sound s) {
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1), s, seconds(30));
};
1시간 뒤에 30초간 울리는 알람을 실행하지만 경보음은 미리 지정되지 않아서 setAlarm에 대해 소리만 전달해주는 인터페이스를 람다로 작성하면 위와 같다.
auto setSoundL = [](Sound s) {
using namespace std::chrono;
using namespace std::literals;
setAlarm(steady_clock::now() + 1h, s, 30s);
};
덧붙여서 C++14는 C++11의 사용자 정의 리터럴 기능에 기초하는 표준 접미사들을 이용하여 코드를 더 간결하게 만들 수 있다.
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
auto setSoundB = std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);
바인드로 작성하면 위와 같은 코드가 된다. 그런데 잘 작동하지 않는다.
람다식에 있는 setAlarm의 평가식(steady_clock::now() + 1h)은 setAlarm이 호출되는 시점에 평가되기 때문에 setAlarm 호출 1시간 뒤 알람이 울리지만 std::bind는 바인드 객체가 생성될 때 평가되어서 setAlarm의 호출이 아닌 std::bind를 호출하는 시점을 기준으로 1시간 뒤 알람이 울린다.
이 문제를 바로잡으려면 바인드 객체가 생성되는 시점이 아니라 실제 setAlarm을 호출하는 시점까지 평가를 지연시켜야 한다. 그러기 위해서는 std::bind안에 두 개의 함수 호출을 포함시켜야 한다.
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
auto setSoundB = std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(), // C++14, std::plus<>()
std::bind(steady_clock::now), hours(1)),
_1,
seconds(30));
현재 시간과 1시간을 더하는 바인드 객체를 매개변수로 넘겨주면 setAlarm이 호출되는 시점으로 지연평가된다.
어찌저찌 해결된것처럼 보이지만 setAlarm 함수를 오버로딩 하게되면 또 문제가 생긴다.
람다는 전혀 문제 없이 컴파일러가 어떤 함수를 호출할지 선택할 수 있지만 std::bind는 함수의 이름만 넘겨받기 때문에 어떤 버전을 호출해야 하는지 선택하지 못해서 컴파일 에러가 발생한다.
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB =
std::bind(static_cast<SetAlarm3ParmType>(setAlarm),
std::bind(std::plus<>(),
std::bind(steady_clock::now),
1h),
_1,
30s);
이런 경우 적절한 함수 포인터로 캐스팅하여 전달해주면 중의성이 해소되어 정상적으로 컴파일 된다.
그런데 이제는 성능상의 차이가 생기기 시작한다.
setSoundL(Sound::Siren);
setSoundB(Sound::Siren);
람다 안의 setAlarm은 인라인화될 가능성이 크지만 바인드 객체에서 setAlarm의 호출은 setAlarm의 함수 포인터를 통해 이루어지므로 인라인화될 가능성이 낮다.
이번에 다룬 상황에서는 setAlarm 호출 하나만 이루어지지만 좀 더 복잡한 코드가 되면 람다의 장점이 더욱 두드러진다.
// 람다
auto betweenL = [lowVal, highVal](const auto& val){ return lowval <= val && cal <= highVal; };
// std::bind
using namespace placeholders;
auto betweenB =
std::bind(std::logical_and<>(),
std::bind(std::less_equal<>(), lowVal, _1),
std::bind(std::less_equal<>(), _1, highVal));
만약 C++11이라면 템플릿 매개변수가 죄다 지정되어야 해서 코드가 더 복잡해진다. 람다 역시 auto 매개변수를 받지 못하기 때문에 타입 지정이 필요하지만 애초에 코드 길이가 차이가 난다.
여담으로 std::bind에 전달되는 placeholders가 아닌 매개변수들은 값으로 저장될까 아니면 참조로 저장될까?
답은 "값으로 전달된다" 이다. 이건 std::bind의 작동 방식을 알고 있어야만 알 수 있는 정보이다. std::bind의 호출 구문으로는 알아낼 수 없다.
하지만 람다는 캡쳐 구문에 명시되어 있기 때문에 한눈에 알 수 있다.
C++14에 들어서는 람다가 std::bind보다 무조건 좋고 C++11의 경우라도 대부분 람다가 좋지만 이동 캡쳐나 보편 참조가 적용된 함수의 경우에는 std::bind의 사용이 정당해진다.
◾ std::bind를 사용하는 것보다 람다가 더 읽기 쉽고 표현력이 좋다. 그리고 더 효율적일 수 있다.
◾ C++14가 아닌 C++11에서는 이동 캡쳐를 구현하거나 객체를 템플릿화된 함수 호출 연산자에 묶으려 할 때 std::bind가 유용할 수 있다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[7장] 항목 36 : 비동기성이 필수일 때에는 std::launch::async를 지정하라 (0) | 2023.01.30 |
---|---|
[7장] 항목 35 : 스레드 기반 프로그래밍보다 작업 기반 프로그래밍을 선호하라 (0) | 2023.01.30 |
[6장] 항목 33 : std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라 (0) | 2023.01.27 |
[6장] 항목 32 : 객체를 클로저 안으로 이동하려면 초기화 캡쳐를 사용하라 (0) | 2023.01.27 |
[6장] 항목 31 : 기본 캡쳐 모드를 피하라 (0) | 2023.01.27 |