ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Lambda와 std::bind
    C++/Effective Modern C++ 2022. 10. 3. 15:57

    std::bind를 간단하게 설명하면 함수에 매개변수를 지정하여 래핑(Wrapping)하는 기능입니다.

    특정 매개변수에 대하여 고정된 값을 설정할 수 있는 부분에서 std::function과는 다른 차별점을 지니고 있습니다.

    std::bind는 실제로 특정 값을 고정한 함수를 만들 때 유용합니다.

    그런데, 람다가 발표되면서 std::bind보다 람다를 쓰는 것이 좋아졌습니다.

    이번 글에서는 람다가 std::bind보다 선호되는 이유에 대하여 살펴보도록 하겠습니다.

     


     

    0. 이번 글의 시나리오

     

    더보기

    알람을 설정하는 함수를 만든다고 가정하도록 하겠습니다.

    // 특정 시간을 지정
    using Time = std::chrono::steady_clock::time_point;
    
    // 특정 알람음을 지정
    enum class Sound { Beep, Siren, Whistle };
    
    // 시간의 길이를 지정
    using Duration = std::chrono::steady_clock::duration;
    
    // 알람 함수
    void setAlram(Time t, Sound s, Duration d);

    위와 같은 알람 함수가 있으며, 이 알람 함수를 이용해서 다음 기능을 만든다고 가정하도록 하겠습니다.

    • 프로그램의 특정 시점에서, 한시간 후 부터 30초간 알람을 울림

    위 기능은 경보음에 대한 설정이 없기 때문에, 경보음을 지정하는 인터페이스를 만들 수 있습니다.

    해당 인터페이스를 람다를 이용해서 작성하면 아래와 같습니다.

    auto setSoundL = [](Sound s) {
        using namespace std::chrono;
        
        setAlarm(steady_clock::now() + hours(1),
                s,
                seconds(30));
        };

    위 함수의 hours(), seconds()대신 표준 접미사 (초 = s, 밀리초 = ms, 시간 = h... etc)를 지정할 수 있는 기능이 C++14에서 추가되었습니다.

    C++14의 추가된 기능을 이용하여 함수를 간략화 하면 아래와 같습니다.

    auto setSoundL = [](Sound s) {
        using namespace std::chrono;
        using namespace std::literals;
        
        setAlarm(steady_clock::now() + 1h,
                s,
                30s);
        };

     

    1. 정확성의 오류 1 : 선언과 호출

     

    더보기

    위 문단의 시나리오에 있는 알람 함수를 std::bind로 작성해보도록 하겠습니다.

    using namespace std::chrono;
    using namespace std::literals;
    
    auto setSoundB = 
        std::bind(setAlarm,
                setady_clock::now() + 1h,
                std::placeholders::_1,
                30s);

    std::bind의 세 번째 인수 std::placeholders는 setSoundB를 호출할 때의 첫 번째 인자를 사용한다는 뜻 입니다.

    그런데, 이 함수에는 문제가 있습니다.

    람다 버전에서와 std::bind버전에서의 첫 번째 인수, steady_clock::now() + 1h의 평가 시점이 다릅니다.

    결론적으로, std::bind버전은 알람 함수의 호출시점이 아닌, std::bind의 호출 시점에서 한 시간이 지난 후에 알람이 울리게 됩니다.

     

    위 문제를 고치기 위해, std::bind함수 내부에 함수를 추가해야 합니다.

    using namespace std::chrono;
    using namespace std::literals;
    
    auto setSoundB = 
        std::bind(setAlarm,
                std::bind(std::plus<>(),
                        std::bind(steady_clock:now),
                        1h),
                std::placeholders::_1,
                30s);

    평가 시점을 변경하기 위해 함수를 추가한 모습입니다.

    한눈에 봐서는 의도를 알아채기 어려워질 만큼 가독성이 떨어진 모습을 볼 수 있습니다.

     

    2. 정확성의 오류 2 : 오버로딩

     

    더보기

    setAlarm함수를 오버로딩 할 경우를 생각해보도록 하겠습니다.

    보음의 크기를 4번째 인자로 받는 setAlarm함수를 만들 경우 다음과 같습니다.

    enum class Volume { Normal, Loud, LoudPlusPlus };
    
    void setAlarm(Time t, Sound s, Duration d, Volume v);

    람다의 경우, 오버로딩과 무관하게 매개변수 3개의 setAlarm이 선택되어 작동합니다.

    하지만 std::bind의 경우 컴파일이 되지 않습니다.

    위 문단의 setSoundB를 다시 가져오도록 하겠습니다.

    auto setSoundB = 
        std::bind(setAlarm,
                std::bind(std::plus<>(),
                        std::bind(steady_clock:now),
                        1h),
                std::placeholders::_1,
                30s);

    이 경우, 컴파일러는 setAlarm의 두 오버로딩 버전 중 어떤 것을 선택해야 할지 알 수 없습니다.

    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),
                std::placeholders::_1,
                30s);

    setAlarm을 3개의 인자를 받는 함수로 캐스팅하여 컴파일 에러를 해결했습니다.

    하지만 이 경우에는 추가적인 문제가 발생합니다.

    컴파일러에 의한 인라인화 (inlining)에 대한 문제입니다.

    • 람다의 경우, 일반적인 함수 호출 연산자로 setAlarm을 호출합니다
    • std::bind의 경우, setAlarm을 가리키는 함수 포인터를 통해 setAlarm을 호출합니다.

    std::bind를 통한 호출의 경우 인라인화 될 가능성이 더 낮기 때문에, 람다보다 속도가 느릴 수 있습니다.

     

    3. 정확성의 오류 3 : 값? 참조?

     

    더보기

    특정 객체의 압축 사본을 만드는 함수를 가정해보도록 하겠습니다.

    enum class CompressLevel { Low, Normal, High };
    
    Widget compress(const Widget& w, CompressLevel lev);

    위 함수를 이용해서, 특정 Widget클래스 w의 압축 수준을 정할 수 있는 함수를 만들어보겠습니다.

    std::bind를 통해 구현할 경우, 아래와 같이 작성될 수 있습니다.

    Widget w;
    auto compressRateB = std::bind(compress, w, std::placeholders::_1);

    이 때, std::bind에 전달된 w가 값으로 저장되는지, 참조로 저장되는지는 매우 중요한 사안입니다.

    std::bind호출과 compressRateB호출 사이에 w의 변화가 일어날 경우, 그 변화가 반영되는지 여부가 결정되기 때문입니다.

    결론적으로 위 경우의 w는 값으로 저장됩니다.

    std::bind는 주어진 인수를 항상 복사하지만, 인수에 std::ref를 적용할 경우 참조로 전달됩니다.

    중요한 것은, std::bind의 위와 같은 특징을 알고 있어야 한다는 것 입니다.

     

    람다의 경우는 값인지, 참조인지 코드에서 명백히 드러납니다.

    auto compressRateL = [w](CompressLevel lev) {
        return compress(w, lev); };

    위 코드에서 w와 lev모두 값으로 전달됩니다.

    그런데, compressRateB를 호출할 때의 인자는 어떻게 전달될까요?

    compressRateB(CompressLevel::High);

    바인드 객체에 전달되는 모든 인수는 함수 호출 연산자가 완벽 전달을 사용하기 때문에, 참조로 전달됩니다.

    이 또한 코드에 드러나지 않고, std::bind의 특징을 알고 있어야 알 수 있는 부분입니다.

     

    4. std::bind를 사용해야 하는 경우

     

    더보기

    위 문단에서 살펴본 람다에 대한 std::bind의 약점들은 다음과 같습니다

    • 가독성이 떨어짐
    • 표현식의 판별 시점이 다름
    • 속도가 느림
    • 명확하지 않음

    위와 같은 약점에도 불구하고, std::bind를 사용하는 것이 합당한 상황이 있습니다.

     

    첫 번째로, 람다의 이동 캡쳐 입니다.

    C++11의 람다는 이동 캡쳐를 지원하지 않기 때문에, 이동이 필요하다면 std::bind를 사용할 만 합니다.

     

    두 번째는 다형성 함수 객체 입니다.

    std::bind로 만들어진 객체는 완벽 전달을 사용하기 때문에, 여러 종류의 인수를 전달할 수 있습니다.

    이는 객체의 템플릿화 된 함수 호출 연산자와 함께할 때 유용합니다.

     

    하지만 위 두 경우, 모두 C++11에 한정된 상황입니다.

    C++14의 경우는 위 두 경우 모두 람다를 사용하는 것이 합당합니다.

     


     

    전체적으로 람다는 std;:bind보다 가독성과 표현력이 좋습니다.

    또한 더 효율적일 수 있으며 (적어도 비 효율적이지는 않습니다) std::bind를 모두 대체할 수 있습니다.

    이를 증명하듯, C++11이 발표된 이후 std::bind는 비 권장 기능이 되었습니다.

    마지막 문단에서 알 수 있듯, C++14부터는 std::bind를 사용하는 것이 합당한 경우가 없습니다.

     

    두 기능 사이의 저울질이 필요한 상황일 경우, 람다를 선호하는 것이 좋겠습니다.

    감사합니다.

    댓글

Designed by Tistory.