ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Lambda : auto 매개변수
    C++/Effective Modern C++ 2022. 9. 25. 13:30

    C++14의 주요 기능 중 하나는, 람다 매개변수에 auto를 사용할 수 있게 된 것입니다.

    이 기능의 구현은 람다의 클로저 클래스의 수정에 있습니다.

    auto f = [](auto x){ return normalize(x); }

    다음과 같은 람다가 있을 때, 이 람다가 만드는 클로저 클래스의 operator()연산자는 다음과 같이 됩니다.

    class CLASS_GENERATED_BY_COMPILER {
    public:
        template<typename T>
        auto operator()(T x) const { return normalize(x); }
        ...
    };

    이 코드는, 호출하는 함수에 따라 문제가 발생할 여지가 있습니다.

    예를 들어 위 예제에서 호출한 normalize함수가 R-value와 L-value를 다른 방식으로 처리한다면, 이 람다는 비정상적으로 작동할 수 있습니다.

    이번 글에서는 위와 같은 문제에 대하여 살펴보도록 하겠습니다.

     


     

    1. 문제에 대한 이해

     

    더보기

    서론의 구현에서, 람다는 normalize에 항상 L-value를 전달합니다.

    만약 서론의 가정대로 normalize가 R-value와 L-value의 처리방식이 다르다면, 주어진 인수가 R-value일 때 R-value를 전달해야 합니다.

     

    간단하게 요약해서, 람다는 normalize에 x를 완벽전달 해야합니다. 

    그렇게 되기 위해서, x는 보편 참조이어야 하고, x를 전달할 때 std::forward를 사용해야 합니다.

    코드를 수정할 경우 개념적으로는 아래와 같습니다.

    auto f = [](auto&& x){ return normalize(std::forward<???>(x)); }

    개념적으로 라는 말을 덧붙인 이유는, std::forward의 템플릿 인수가 문제이기 때문입니다.

    템플릿 함수의 경우 형식 매개변수 T를 사용할 수 있지만, 람다에서는 그렇지 못합니다.

     

    위와 같이 템플릿 형식을 사용할 수 없을 때 사용할 수 있는 수단은 decltype()이 있습니다.

    하지만 decltype도 표면적으로는 문제가 발생합니다.

     

    다음 문단에서 이어집니다.

     

    2. std::forward와 decltype

     

    더보기

    std::forward호출 시 전달할 인수가 L-value임을 나타내기 위해서는 L-value참조 형식 인수를 사용하고, R-value형식을 나타내기 위해서는 비 참조 형식 인수를 사용하는 것이 관례입니다.

    하지만 이번 경우 (decltype을 사용한 경우)에는 이것이 충족되지 않습니다.

    • x가 L-value일 경우, decltype(x)는 L-value참조를 산출합니다.
    • x가 R-value일 경우, decltype(x)는 R-value참조를 산출합니다.

    std::forward에 대하여 R-value는 비 참조 형식을 사용해야 하는데, 이는 관례와 맞지 않습니다.

     

    3. 증명

     

    더보기

    std::forward의 C++14 구현을 살펴보도록 하겠습니다.

    template<typename T>
    T&& forward(remove_reference_t<T>& param) {
        return static_cast<T&&>(param);
    }

    위 구현에 대하여, 클라이언트가 MyClass라는 형식의 R-value를 완벽하게 전달할 때에는 MyClass형식으로 std::forward가 인스턴스화 할 것입니다.

    그 때의 std::forward 템플릿은 다음과 같이 인스턴스화 합니다.

    MyClass&& forward(MyClass& param) {
        return static_cast<MyClass&&>(param);
    }

    여기서, 클라이언트 코드가 MyClass형식의 동일한 R-value를 완벽 전달하고, T를 비참조 형식으로 지정하는 관례를 따르지 않은 채 R-value참조 형식으로 지정할 경우를 생각해보도록 하겠습니다.

     

    요약하자면 T를 MyClass&&로 지정하는 경우이고, 이번 글의 decltype의 경우입니다.

    MyClass&& && forward(MyClass& param) {
        return static_cast<MyClass&& &&>(param);
    }

    이 문맥에는 참조 축약이 적용될 수 있고, 최종적으로는 다음과 같은 모습이 됩니다.

    MyClass&& forward(MyClass& param) {
        return static_cast<MyClass&&>(param);
    }

    이 인스턴스와 std::forward<MyClass> 인스턴스 (= T가 MyClass일 경우)를 비교해볼 경우, 동일하게 연역됩니다.

    즉, R-value참조 형식으로 std::forward를 인스턴스화 한 결과는 비참조 형식으로 인스턴스화 한 결과와 같습니다.

     

    4. 결론

     

    더보기

    람다에 R-value가 전달되었을 때 decltype(x)가 산출하는 형식이 관례와 맞지 않더라도 (비참조가 아니더라도) 관례적 형식을 사용했을 때와 동일한 결과가 도출됩니다.

     

    따라서, R-value와 L-value모두 decltype(x)를 std::forward에 전달할 경우 완벽 전달이 성립됩니다.

    완벽 전달을 이용해서 보완한 서론의 코드는 아래와 같이 완성될 수 있습니다.

    // Before
    auto f = [](auto x){ return normalize(x); }
    
    // After
    auto f = [](auto&& x){ return normalize(std::forward<decltype(x)>(x)); }

     


     

    결론은 std::forward를 통해 auto&&매개변수를 전달할 때는 std::forward를 사용하는 것이 바람직하다 입니다.

    본문의 문제 상황 파악, 해결에 사용된 std::forward, 보편 참조와 같은 내용은 다른 글에 기술되어 있습니다.

    감사합니다.

    댓글

Designed by Tistory.