스토리텔링 개발자

[Effective Modern C++] 34. std::bind 대신 람다 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 34. std::bind 대신 람다

김디트 2025. 4. 8. 11:21
728x90

항목 34. std::bind보다 람다를 선호하라

 

 

 

이유 1 : 가독성
  • 람다가 가독성이 더 좋다.
using Time = std::chrono::steady_clock::time_point;
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버전에서는 리터럴 suffix를 이용해서 더 줄일 수 있다.
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;

using namespace std::placeholders; // _1을 사용하기 위해

// 허나 문제가 있다!
auto setSoundB = std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);
  • steady_clock::now() 가 실행 시점이 아니라 바인딩 되는 시점에 호출된다.
    • 결과적으로, 경보는 setAlarm 호출 후 한시간이 지난 시점이 아니라
    • std::bind 호출 후 한시간이 지난 시점에 울린다.
  • 해결하려면 setAlarm 호출 때까지 지연시키라고 std::bind에게 알려줘야 한다.
audo setSoundB = std::bind(setAlarm,
                           std::bind(std::plus<>(),
                                     std::bind(steady_clock::now),
                                     1h),
                           _1,
                           30s);
  • std::plus<>
    • 타입이 들어가지 않았다.
    • C++14에서는 표준 연산자 템플릿에 대한 템플릿 타입 인수를 생략할 수 있다.
    • C++11은 즉 아래처럼 구현해야 한다.
audo setSoundB = std::bind(setAlarm,
                           std::bind(std::plus<steady_clock::time_point>(),
                                     std::bind(steady_clock::now),
                                     1h),
                           _1,
                           30s);
  • 무엇이 가독성이 좋은지는... 자명하다.

 

 

 

이유 2 : 오버로드
  • 음량을 네 번째 매개변수로 받는 버전을 추가한다고 하면?
enum class Volume { Normal, Loud, LoudPlusPlus };

void setAlarm(Time t, Sound s, Duration d, Volume v);
  • 람다는 문제없이 동작한다.
    • 인수 세 개짜리 버전을 알아서 선택하여 호출하기 때문이다.
  • std::bind 버전은 컴파일되지 않는다.
    • 컴파일러는 두 버전 중 어떤 것을 std::bind에 넘겨주어야 할지 결정할 방법이 없다.
    • 컴파일러는 함수 이름밖에 모르기 때문이다.
    • 이를 해결하기 위해서는 적절한 함수 포인터 타입으로 캐스팅해야 한다.
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);

// 캐스팅 추가
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
                           std::bind(std::plus<>(),
                                     std::bind(steady_clock::now),
                                     1h),
                           _1,
                           30s);

 

 

 

좀 더 복잡한 코드
// C++14 버전
audo betweenL = [lowVal, highVal](const auto& val)
{
    return lowVal <= val && val <= highVal;
};


// C++11 버전
audo betweenL = [lowVal, highVal](int val)
{
    return lowVal <= val && val <= highVal;
};
  • std::bind 로도 표현은 가능하지만, 이해가 너무 힘들다.
using namespace std::placeholders;

// C++14 버전
audo betweenB = std::bind(std::logical_and<>(),
                          std::bind(std::less_equal<>(), lowVal, _1),
                          std::bind(std::less_equal<>(), _1, highVal));
                          
// C++11 버전
audo betweenB = std::bind(std::logical_and<bool>(),
                          std::bind(std::less_equal<int>(), lowVal, _1),
                          std::bind(std::less_equal<int>(), _1, highVal));

 

 

 

 

이유 3 : 캡쳐 방식 가독성
enum class CompLevel { Low, Normal, High };

Widget Compress(const Widget& w, CompLevel lev);
  • std::bind 버전
Widget w;

// 람다 버전
// w는 명백히 값으로 캡쳐된다.
auto compressRateL = [w](CompLevel lev) { return compress(w, lev); };

// std::bind 버전
using namespace std::placeholders;
// 여기서 w는 어떻게 저장될까?(값? 참조?)
auto compressRateB = std::bind(compress, w, _1);
  • std::bind 버전에서, w는 값으로 전달된다.
    • 하지만 이를 알기 위해서는 반드시 std::bind의 작동 방식을 꿰고 있어야 한다.
    • 호출 구문 자체로는 그 사실을 추론할 수 없다.
  • 또한 호출 시 인수를 전달할 때에도 가독성 문제가 있다.
// 람다 버전
compressRateL(CompLevel::High); // 인수는 명확히 값으로 전달된다.
// 명세 '(CompLevel lev)'를 보면 명확히 값임을 알 수 있다.

// std::bind 버전
compressRateB(CompLevel::High); // 어떻게 전달될까??
  • std::bind 버전에서 인수는 참조로 전달된다.
    • 함수 호출 연산자가 완벽 전달을 사용하기 때문이다.
    • 이 역시 알기 위해서는 std::bind의 동작 방식을 꿰고 있어야 한다.

 

 

 

std::bind가 유용한 경우
  • 이동 캡쳐(move capture)
    • C++11 람다는 이동 캡쳐를 지원하지 않는다.
    • 이를 흉내낼 때 사용할 수 있다.(항목 32 참조)
  • 다형적 함수 객체(polymorphic function object)
    • 바인드 객체에 대한 함수 호출 연산자는 완벽 전달을 사용한다.
    • 그러므로 어떤 타입의 인수도 받을 수 있다.(항목 30의 완벽 전달 제약 안에서)
    • 즉, 객체를 템플릿화된 함수 호출 연산자와 묶으려 할 때 유용하다.
    • 이 역시 C++11에서만 유용하다.
class PolyWidget
{
public:
    template<typename T>
    void operator()(const T& param) const;
    ...
};

// 이렇게 묶으면 어떤 타입도 받을 수 있게 된다.
PolyWidget pw;
auto boundPW = std::bind(pw, _1);

boundPW(1930); // int 전달
boundPW(nullptr); // nullptr 전달
boundPW("Rosebud"); // 문자열 전달
  • C++14에서는 매개변수로 auto를 사용할 수 있으므로 훨씬 간단하다.
auto boundPW = [pw](const auto& param) { pw(param); };

 

728x90
Comments