C ++ 11 람다 구현 및 메모리 모델


97

C ++ 11 클로저에 대해 올바르게 생각 std::function하는 방법과 구현 방법 및 메모리 처리 방법 에 대한 정보를 원합니다 .

나는 조기 최적화를 믿지 않지만 새로운 코드를 작성하는 동안 내 선택의 성능 영향을 신중하게 고려하는 습관이 있습니다. 또한 비 결정적 메모리 할당 / 할당 해제 일시 중지를 피해야하는 마이크로 컨트롤러 및 오디오 시스템에서 상당한 양의 실시간 프로그래밍을 수행합니다.

따라서 C ++ 람다를 사용하거나 사용하지 않을 때를 더 잘 이해하고 싶습니다.

내 현재 이해는 캡처 된 클로저가없는 람다가 C 콜백과 똑같다는 것입니다. 그러나 환경이 값 또는 참조로 캡처되면 스택에 익명 개체가 생성됩니다. 값 폐쇄가 함수에서 반환되어야 할 때 하나는 그것을 std::function. 이 경우 클로저 메모리는 어떻게됩니까? 스택에서 힙으로 복사됩니까? 가 해제 될 때마다 std::function해제 std::shared_ptr됩니까?

실시간 시스템에서 람다 함수 체인을 설정하여 B를 A에 연속 인수로 전달하여 처리 파이프 라인 A->B을 만들 수 있다고 상상합니다 . 이 경우 A 및 B 클로저는 한 번 할당됩니다. 스택 또는 힙에 할당되는지 확실하지 않지만. 그러나 일반적으로 실시간 시스템에서 사용하는 것이 안전 해 보입니다. 반면에 B가 반환하는 람다 함수 C를 생성하면 C에 대한 메모리가 반복적으로 할당되고 할당 해제되므로 실시간 사용에 적합하지 않습니다.

의사 코드에서 DSP 루프는 실시간으로 안전 할 것이라고 생각합니다. A가 해당 인수를 호출하는 블록 A와 B를 처리하고 싶습니다. 이 두 함수는 모두 std::function객체를 반환 하므로 해당 환경이 힙에 저장 f되는 std::function객체가됩니다.

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

그리고 실시간 코드에서 사용하는 것이 나쁘다고 생각하는 것 :

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

그리고 스택 메모리가 클로저에 사용될 가능성이 있다고 생각하는 곳 :

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

후자의 경우 루프가 반복 될 때마다 클로저가 생성되지만 이전 예제와 달리 함수 호출과 같으므로 힙 할당이 이루어지지 않기 때문에 저렴합니다. 또한 컴파일러가 클로저를 "리프트"하고 인라인 최적화를 수행 할 수 있는지 궁금합니다.

이 올바른지? 감사합니다.


4
람다 식을 사용할 때 오버 헤드가 없습니다. 다른 선택은 정확히 똑같은 함수 객체를 직접 작성하는 것입니다. 인라인 질문에 대한 Btw는 컴파일러가 필요한 모든 정보를 가지고 있기 때문에 operator(). 수행 할 "리프팅"은 없습니다. 람다는 특별한 것이 아닙니다. 그것들은 지역 함수 객체의 약자 일뿐입니다.
Xeo

이것은 std::function상태를 힙에 저장 하는지 여부에 대한 질문으로 보이며 람다와 관련이 없습니다. 맞습니까?
Mooing Duck 2012-08-30

8
그냥 오해의 경우에 그것을 밖으로 철자 : 람다 표현은 하지std::function !
Xeo

1
참고 : 함수에서 람다를 반환 할 때주의해야합니다. 참조로 캡처 한 모든 지역 변수는 람다를 생성 한 함수를 떠난 후 유효하지 않게되기 때문입니다.
Giorgio

2
@Steve C ++ 14 이후로 auto반환 유형이 있는 함수에서 람다를 반환 할 수 있습니다 .
Oktalist

답변:


104

내 현재 이해는 캡처 된 클로저가없는 람다가 C 콜백과 똑같다는 것입니다. 그러나 환경이 값 또는 참조로 캡처되면 스택에 익명 개체가 생성됩니다.

아니; 그것은 항상 스택에 생성 알 수없는 유형의 C ++ 객체. 캡쳐리스 람다 수 변환 (이것은 C가 호출 규칙에 적합한 지 구현 의존하지만) 함수 포인터에 있지만, 즉 의미하지 않는다 함수 포인터.

함수에서 값 클로저를 반환해야 할 때 std :: function으로 래핑합니다. 이 경우 클로저 메모리는 어떻게됩니까?

람다는 C ++ 11에서 특별한 것이 아닙니다. 다른 물체와 같은 물체입니다. 람다 식은 스택에서 변수를 초기화하는 데 사용할 수있는 임시를 생성합니다.

auto lamb = []() {return 5;};

lamb스택 객체입니다. 생성자와 소멸자가 있습니다. 그리고 그것은 모든 C ++ 규칙을 따를 것입니다. 유형 lamb에는 캡처 된 값 / 참조가 포함됩니다. 다른 유형의 다른 개체 멤버와 마찬가지로 해당 개체의 멤버가됩니다.

당신은 그것을 줄 수 있습니다 std::function:

auto func_lamb = std::function<int()>(lamb);

이 경우 값의 복사본 을 얻습니다 lamb. lamb값으로 무엇이든 캡처 했다면 해당 값의 복사본이 두 개있을 것입니다. 하나는 lamb, 하나는 func_lamb.

현재 범위가 끝나면 스택 변수를 정리하는 규칙에 func_lamb따라 삭제되고 뒤에 lamb.

힙에 쉽게 할당 할 수 있습니다.

auto func_lamb_ptr = new std::function<int()>(lamb);

의 내용에 대한 메모리가 정확히 어디에 있는지는 std::function구현에 따라 다르지만 std::function일반적으로에서 사용하는 유형 삭제에는 적어도 하나의 메모리 할당이 필요합니다. 이것이 std::function의 생성자가 할당자를 취할 수있는 이유 입니다.

std :: function이 해제 될 때마다 해제됩니까? 즉, std :: shared_ptr처럼 참조 계산됩니까?

std::function내용 의 사본 을 저장합니다 . 거의 모든 표준 라이브러리 C ++ 유형과 마찬가지로 값 의미 체계를function 사용 합니다 . 따라서 복사 가능합니다. 복사 할 때 새 function개체는 완전히 분리됩니다. 또한 이동 가능하므로 추가 할당 및 복사없이 내부 할당을 적절하게 전송할 수 있습니다.

따라서 참조 카운팅이 필요하지 않습니다.

"메모리 할당"이 "실시간 코드에서 사용하기에 좋지 않음"과 같다고 가정하면 다른 모든 내용이 정확합니다.


1
훌륭한 설명, 감사합니다. 따라서 생성은 std::function메모리가 할당되고 복사되는 지점입니다. (스택에 할당 되었기 때문에) 클로저를 반환 할 수있는 방법이없는 것 같습니다 std::function.
Steve

3
@Steve : 예; 범위를 벗어나려면 일종의 컨테이너에 람다를 래핑해야합니다.
Nicol Bolas

전체 함수의 코드가 복사됩니까? 아니면 원래 함수가 컴파일 시간에 할당되고 닫힌 값을 전달합니까?
Llamageddon

람다가 아무것도 캡처 std::function하지 않으면 동적 메모리없이 객체에 저장 될 수 있도록 표준이 다소 간접적으로 명령한다고 추가하고 싶습니다 (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5). 할당이 진행 중입니다.
5gon12eder

2
@Yakk : "대형"을 어떻게 정의합니까? 두 포인터의 상태가 "대"인 객체입니까? 3 개 또는 4 개는 어떻습니까? 또한 개체 크기가 유일한 문제는 아닙니다. 객체가 이동 불가능한 경우 noexcept 이동 생성자가 있으므로 할당에 저장 해야합니다function . "일반적으로 필요"라는 말의 요점은 " 항상 필요" 라고 말하는 것이 아니라 할당이 수행되지 않는 상황이 있다는 것입니다.
Nicol Bolas

1

C ++ 람다 과부하로 단지 문법 설탕 주위 (익명) 펑터 클래스입니다 operator()std::functioncallables 주변 단지 래퍼 (즉 펑, 람다, C-기능, ...) 않는 값으로 사본을 현재의에서 "고체 람다 객체를" 스택 범위- 힙까지 .

실제 생성자 / 재배치 수를 테스트하기 위해 테스트를 수행했습니다 (shared_ptr에 다른 수준의 래핑을 사용하지만 사례는 아님). 직접 확인 :

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

다음과 같이 출력됩니다.

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

스택 할당 람다 객체에 대해 정확히 동일한 ctor / dtor 세트가 호출됩니다! (이제 스택 할당을 위해 Ctor를 호출하고 std :: function에서 생성하기 위해 Copy-ctor (+ 힙 할당)를 호출하고 shared_ptr 힙 할당 + 함수 생성을 위해 또 다른 하나를 호출합니다)

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.