ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Lambda : Init capture
    C++/Effective Modern C++ 2022. 9. 20. 09:08

    람다를 사용하다 보면 값 캡쳐도 참조 캡쳐도 적절하지 않을 때가 있습니다.

    std::unique_ptr등의 이동 전용 객체를 사용하거나, 로직을 최적화 하기 위해 이동 연산을 사용하려 할 때가 그렇습니다.

    위와 같은 경우는 객체를 클로저 내부로 이동시키는 것이 바람직한데, C++11에서는 그것이 용이하지 않습니다.

    하지만 용이하지만 않을 뿐, 이동 연산을 흉내내는 방법이 있으며, C++14에서는 그것을 정식으로 지원합니다.

     

    이번 글에서는 람다에 이동을 지원하는 매커니즘인 초기화 캡쳐 (Init capture)에 대하여 살펴보도록 하겠습니다.

     


     

    1. Init capture

     

    더보기

    다음은 초기화 캡쳐를 사용하는 예제입니다.

    auto ptr = std::make_unique<MyClass>();
    ...
    auto func = [ptr = std::move(ptr)] {
        // Logic
        };

    위 예제의 람다의 캡쳐 절에 사용된 문맥이 초기화 캡쳐 입니다.

    특이 사항으로, 좌변과 우변의 스코프가 다르다는 점이 있습니다. (좌변은 클로저의 스코프, 우변은 람다가 정의되는 스코프)

     

    추가로 예제를 하나 더 살펴보겠습니다.

    auto func = [ptr = std::make_unique<MyClass>()]{
        // Logic
        };

    위 예제는 기존 객체를 클로저로 이동시키는 것이 아니라, 클로저의 자료 멤버를 직접 초기화하는 예제입니다.

    특이 사항으로, 특정 표현식의 결과를 캡쳐했다는 점이 있습니다.

     

    두 상황 모두 C++11에서는 불가능했던 문맥입니다.

    C++14에서 위와 같은 표현이 허용된 것으로, 캡쳐에 관한 개념이 더욱 일반화 되었다고 볼 수 있습니다.

    이러한 점에서 Init capture를 Generalized lambda capture라고 부르기도 합니다.

     

    2. 람다를 클래스로

     

    더보기

    람다 표현식은 컴파일러가 하나의 클래스를 자동으로 작성해서 그 클래스의 객체를 생성하게 만드는 수단입니다.

    따라서, 람다로 할 수 있는 모든 것을 그런 클래스를 만들어서 수행하는 것이 가능합니다.

     

    만약 컴파일러의 버전 등의 이슈로 위 문단의 캡쳐가 불가능하더라도, 아래오 같이 클래스로 구현할 수 있습니다.

    class MyClass2 {
    public:
        explicit MyClass2(MyClass&& ptr) : ptr(std::move(ptr)) {}
        void operator()() const {
            // Logic
        }
    private:
        MyClass ptr;
    };
    
    auto func = MyClass2(std::make_unique<MyClass>());

    람다를 만드는 것 보다 코드가 길어졌지만, C++11에서도 사용 가능하다는 점에서 눈여겨 볼 만 합니다. 

     

    3. C++11에서 Init capture 흉내내기

     

    더보기

    2번 문단의 내용으로도 초기화 캡쳐를 구현할 수 있지만, 반드시 람다를 사용해야 하는 경우에는 아래와 같이 구현할 수 있습니다.

    1. 캡쳐 할 객체를 std::bind가 산출하는 함수 객체로 이동합니다.
    2. 그 캡쳐된 객체에 대한 참조를 람다에 넘겨줍니다.

     

    지역 std::vector를 생성 후 값을 추가하여 클로저 안으로 이동하는 예제를 생각해보도록 하겠습니다.

    C++14에서는 아래와 같이 구현할 수 있습니다.

    std::vector<int> data;
    ... // Add data
    auto func = [data = std::move(data)] {
        // Logic
        };

    위와 같은 내용을 C++11에서 구현할 경우 아래와 같습니다.

    std::vector<int> data;
    ... // Add data
    auto func = std::bind(
        [](const std::vector<int>& data) {
            // Logic
            },
        std::move(data)
        );

    람다 표현식과 유사하게, std::bind는 함수 객체를 산출합니다.

    이 함수 객체는 L-value인수에 대해 복사 생성된 객체, R-value인수에 대해 이동 생성된 객체를 가질 수 있습니다.

    위 예제에서는 std::move(data)가 R-value이므로 이동 생성된 객체가 클로저에 생성됩니다.

    그리고 이는 C++11 람다에서 객체의 이동이 불가능한 한계를 우회하는 결과가 됩니다.

     

    엄밀히 말하면 func는 std::bind가 생성한 함수 객체이고, func가 호출되면 (operator()연산자로 호출되면) func에 저장된 data의 복사본이 std::bind에 지정된 람다에 전달됩니다.

    약간의 차이가 존재하지만, 함수 객체의 수명과 클로저의 수명이 같으므로 함수 객체를 클로저로 취급하는 것이 가능합니다.

     

    4. std::bind와 const

     

    더보기

    기본적으로 람다로부터 만들어진 클로저 클래스의 operator() 멤버 함수는 const입니다.

    따라서, 람다 본문 내에서 클로저의 모든 자료 멤버는 const가 됩니다.

    하지만, std::bind로 생성된 함수 객체에 전달되는 매개변수는 const가 아닙니다.

     

    이를 완전히 람다와 동일하게, 수정되지 않게 하려면 매개변수를 const에 대한 참조로 선언해야 합니다.

    문단3의 예제에서 매개변수를 const에 대한 참조로 설정한 것은 이와 같은 이유입니다.

     

    만약 수정 가능한 람다를 사용한다면 (람다를 mutable로 선언한다면) const를 제거해야합니다.

    auto func = std::bind(
        [](std::vector<int>& data) mutable {
            // Logic
            },
        std::move(data)
        );

    문단 3의 예제의 람다를 mutable로 선언한 예제입니다.

     


     

    Init capture는 기본 캡쳐를 제외한 람다의 모든 캡쳐를 할 수 있습니다.

    기본 캡쳐는 지양해야 될 기능이니, Init capture는 캡쳐를 명시적으로 지정할 때 유용합니다.

    하지만 표현식이 길어지기 때문에 코드가 장황해질 수 있으니, C++11의 캡쳐로 가능한 것은 C++11의 것을 사용하는 것 또한 합당합니다.

     

    감사합니다.

    댓글

Designed by Tistory.