Abseil Tip 108 std::bind를 피하세요

Roman Perepelitsa (roman.perepelitsa@gmail.com) 작성
최초 게시일: 2016년 1월 7일
최종 업데이트: 2020년 8월 19일

빠른 링크: abseil.io/tips/108


std::bind를 피하세요

이 팁에서는 코드 작성 시 std::bind()를 피해야 하는 이유를 요약합니다.


std::bind를 피해야 하나요?

std::bind()를 올바르게 사용하는 것은 어렵습니다. 몇 가지 예를 살펴보겠습니다. 아래 코드가 좋아 보이나요?

void DoStuffAsync(std::function<void(absl::Status)> cb);

class MyClass {
  void Start() {
    DoStuffAsync(std::bind(&MyClass::OnDone, this));
  }
  void OnDone(absl::Status status);
};

많은 숙련된 C++ 개발자들이 위와 같은 코드를 작성하지만 컴파일되지 않는다는 사실을 발견하곤 합니다. std::function<void()>와 함께 사용할 때는 동작하지만, MyClass::OnDone에 추가 매개변수를 넣으면 오류가 발생합니다. 왜 그럴까요?


문제: std::bind()는 단순히 첫 N개의 인수를 바인딩하지 않습니다.

std::bind()는 많은 개발자들이 기대하는 것처럼 단순히 첫 N개의 인수를 바인딩하지 않습니다(이 동작은 부분 함수 적용이라고 합니다). 대신 모든 인수를 명시적으로 지정해야 합니다. 올바른 std::bind() 사용법은 다음과 같습니다:

std::bind(&MyClass::OnDone, this, std::placeholders::_1)

너무 번거롭죠? 더 나은 방법이 있을까요? 네, 있습니다. 바로 std::bind_front()를 사용하는 것입니다. (C++20을 사용할 수 없는 경우, absl::bind_front를 사용할 수 있습니다.)

std::bind_front(&MyClass::OnDone, this)

부분 함수 적용: std::bind()는 하지 않지만, std::bind_front()는 합니다.

std::bind_front()는 첫 N개의 인수를 바인딩하고 나머지를 완벽하게 전달합니다. 예를 들어, std::bind_front(F, a, b)(x, y)F(a, b, x, y)로 평가됩니다.


또 다른 문제

다음 코드를 봅시다:

void DoStuffAsync(std::function<void(absl::Status)> cb);

class MyClass {
  void Start() {
    DoStuffAsync(std::bind(&MyClass::OnDone, this));
  }
  void OnDone();  // 여기에 absl::Status가 없습니다.
};

OnDone()은 매개변수를 받지 않지만 DoStuffAsync() 콜백은 absl::Status를 필요로 합니다. 컴파일 오류가 날 것 같지만, 이 코드는 경고 없이 컴파일됩니다. 이는 std::bind()가 과도하게 문제를 해결하려고 하기 때문입니다.

이 코드에서는 DoStuffAsync()에서 발생한 잠재적 오류가 조용히 무시됩니다. 이는 IO 작업이 성공한 것처럼 보이게 만들 수 있으며, 이는 심각한 문제를 초래할 수 있습니다.


std::bind의 다른 문제

  1. std::bind()는 컴파일 타임 검사를 무효화합니다.
    일반적으로 컴파일러는 호출자가 예상보다 많은 인수를 전달하면 알려줍니다. 하지만 std::bind()에서는 그렇지 않습니다.

  2. std::bind()는 이동 전용 객체를 올바르게 처리하지 못합니다.
    다음 코드는 컴파일되지 않습니다:

    void Process(std::unique_ptr<Request> req);
    
    void ProcessAsync(std::unique_ptr<Request> req) {
      thread::DefaultQueue()->Add(
          ToCallback(std::bind(&MyClass::Process, this, std::move(req))));
    }
    

    대신 std::bind_front()를 사용하여 문제를 해결할 수 있습니다.

  3. std::bind()는 중첩 호출에서 예상치 못한 동작을 합니다.
    중첩된 std::bind() 호출은 의도와 다르게 동작할 수 있습니다. std::bind() 대신 람다나 명명된 함수를 사용하는 것이 더 좋습니다.


std::bind 대신 무엇을 사용해야 하나요?

  1. 람다(lambda):
    std::bind()를 람다로 대체하면 가독성과 안전성이 향상됩니다.

    std::bind(&MyClass::OnDone, this)
    

    를 다음과 같이 바꿀 수 있습니다:

    [this]() { OnDone(); }
    
  2. std::bind_front:
    부분 함수 적용에는 std::bind_front를 사용하는 것이 좋습니다.

    std::bind(&MyClass::OnDone, this, std::placeholders::_1)
    

    를 다음과 같이 바꿀 수 있습니다:

    std::bind_front(&MyClass::OnDone, this)
    

결론

std::bind()를 피하세요. 대신 람다나 std::bind_front를 사용하세요.


추가 자료

  • Effective Modern C++, Item 34: Prefer lambdas to std::bind

이 번역이 도움이 되길 바랍니다! 😊