KoreanFoodie's Study

Effective Modern C++ | 항목 34 : std::bind 보다 람다를 선호하라 본문

Tutorials/C++ : Advanced

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 에서는 사용하는 것이 합당한 경우가 전혀 없다는 것을 잘 알아두자.

Comments