[c] 함수 포인터, 클로저 및 Lambda

지금 막 함수 포인터에 대해 배우고 있습니다.이 주제에 대한 K & R 장을 읽으면서 가장 먼저 느꼈던 것은 “이봐, 이건 마치 클로저와 같다”는 것이었다. 나는이 가정이 근본적으로 잘못되었다는 것을 알았고 온라인에서 검색 한 후이 비교에 대한 분석을 실제로 찾지 못했습니다.

그렇다면 C 스타일 함수 포인터가 클로저 또는 람다와 근본적으로 다른 이유는 무엇입니까? 내가 말할 수있는 한, 함수 포인터가 함수를 익명으로 정의하는 관행과는 반대로 정의 된 (명명 된) 함수를 가리키고 있다는 사실과 관련이 있습니다.

함수에 함수를 전달하는 것이 이름이 지정되지 않은 두 번째 경우에서 전달되는 정상적인 일상적인 함수 인 첫 번째 경우보다 더 강력한 것으로 보이는 이유는 무엇입니까?

두 사람을 그렇게 면밀히 비교하는 것이 어떻게 그리고 왜 잘못된 것인지 말해주세요.

감사.



답변

람다 (또는 클로저 )는 함수 포인터와 변수를 모두 캡슐화합니다. 이것이 C #에서 다음을 수행 할 수있는 이유입니다.

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

익명 대리자를 클로저로 사용했습니다 (구문이 람다에 해당하는 것보다 조금 더 명확하고 C에 더 가깝습니다). 클로저에 lessThan (스택 변수)을 캡처했습니다. 클로저가 평가 될 때 lessThan (스택 프레임이 파괴되었을 수 있음)은 계속 참조됩니다. lessThan을 변경하면 비교를 변경합니다.

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

C에서 이것은 불법입니다.

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

2 개의 인수를받는 함수 포인터를 정의 할 수 있지만 :

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

그러나 이제 나는 그것을 평가할 때 2 개의 인수를 전달해야합니다. 이 함수 포인터를 lessThan이 범위에 포함되지 않은 다른 함수로 전달하려면 체인의 각 함수에 전달하거나 전역으로 승격하여 수동으로 유지해야합니다.

클로저를 지원하는 대부분의 주류 언어는 익명 함수를 사용하지만 이에 대한 요구 사항은 없습니다. 익명 함수없이 클로저를 가질 수 있고 클로저없이 익명 함수를 가질 수 있습니다.

요약 : 클로저는 함수 포인터 + 캡처 된 변수의 조합입니다.


답변

‘실제’클로저를 사용하거나 사용하지 않는 언어에 대한 컴파일러를 작성한 사람으로서 위의 답변 중 일부에 정중하게 동의하지 않습니다. Lisp, Scheme, ML 또는 Haskell 클로저 는 새 함수를 동적으로 생성하지 않습니다 . 대신 기존 함수를 재사용 하지만 새로운 자유 변수를 사용 합니다. 자유 변수 모음은 적어도 프로그래밍 언어 이론가에 의해 종종 환경 이라고 불립니다 .

클로저는 함수와 환경을 포함하는 집계 일뿐입니다. New Jersey 컴파일러의 표준 ML에서 우리는 하나를 레코드로 표현했습니다. 한 필드에는 코드에 대한 포인터가 포함되고 다른 필드에는 자유 변수 값이 포함됩니다. 컴파일러 는 동일한 코드에 대한 포인터를 포함 하지만 자유 변수에 대해 다른 값을 포함하는 새 레코드를 할당하여 동적으로 새 클로저 (함수 아님)를 생성했습니다 .

이 모든 것을 C에서 시뮬레이션 할 수 있지만 엉덩이에 고통이 있습니다. 두 가지 기술이 널리 사용됩니다.

  1. 함수 (코드)에 대한 포인터와 자유 변수에 대한 별도의 포인터를 전달하여 클로저가 두 C 변수로 분할되도록합니다.

  2. 구조체에 대한 포인터를 전달합니다. 여기서 구조체에는 자유 변수의 값과 코드에 대한 포인터가 포함됩니다.

기술 # 1은 C에서 어떤 종류의 다형성 을 시뮬레이션하려고하는데 환경의 유형을 밝히고 싶지 않을 때 이상적 입니다. 환경을 나타 내기 위해 void * 포인터를 사용합니다. 예를 들어 Dave Hanson의 C 인터페이스 및 구현을 참조하십시오 . 기능 언어에 대한 네이티브 코드 컴파일러에서 발생하는 것과 더 유사한 기술 # 2는 또 다른 익숙한 기술인 가상 멤버 함수가있는 C ++ 객체와 유사합니다. 구현은 거의 동일합니다.

이 관찰은 Henry Baker의 현명한 균열로 이어졌습니다.

Algol / Fortran 세계의 사람들은 미래의 효율적인 프로그래밍에서 가능한 사용 함수 클로저가 무엇인지 이해하지 못했다고 수년간 불평했습니다. 그런 다음 ‘객체 지향 프로그래밍’혁명이 일어 났고 이제는 모든 프로그램이 함수 클로저를 사용하는 것을 제외하고는 여전히이를 호출하기를 거부합니다.


답변

C에서는 함수를 인라인으로 정의 할 수 없으므로 실제로 클로저를 만들 수 없습니다. 당신이하는 일은 미리 정의 된 메서드에 대한 참조를 전달하는 것뿐입니다. 익명 메서드 / 클로저를 지원하는 언어에서는 메서드 정의가 훨씬 더 유연합니다.

가장 간단한 용어로, 함수 포인터에는 (전역 범위를 계산하지 않는 한) 관련된 범위가 없지만 클로저에는이를 정의하는 메서드의 범위가 포함됩니다. 람다를 사용하면 메서드를 작성하는 메서드를 작성할 수 있습니다. 클로저를 사용하면 “일부 인수를 함수에 바인딩하고 결과적으로 낮은 인수 함수를 얻을 수 있습니다.” (Thomas의 의견에서 발췌). C에서는 그렇게 할 수 없습니다.

편집 : 예제 추가 (지금 내 마음에있는 Actionscript-ish 구문 원인을 사용할 것입니다) :

다른 메서드를 인수로 사용하지만 호출 될 때 해당 메서드에 매개 변수를 전달하는 방법을 제공하지 않는 메서드가 있다고 가정 해 보겠습니다. 예를 들어 전달한 메서드를 실행하기 전에 지연을 유발하는 메서드 (어리석은 예이지만 간단하게 유지하고 싶습니다).

function runLater(f:Function):Void {
  sleep(100);
  f();
}

이제 runLater () 사용자가 객체의 일부 처리를 지연 시키길 원한다고 가정합니다.

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

process ()에 전달하는 함수는 더 이상 정적으로 정의 된 함수가 아닙니다. 동적으로 생성되며 메서드가 정의되었을 때 범위에 있던 변수에 대한 참조를 포함 할 수 있습니다. 따라서 ‘o’및 ‘objectProcessor’는 전역 범위에 있지 않더라도 액세스 할 수 있습니다.

이해가 되셨기를 바랍니다.


답변

폐쇄 = 논리 + 환경.

예를 들어 다음 C # 3 방법을 고려하십시오.

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

람다 식은 논리 ( “이름 비교”)뿐만 아니라 매개 변수 (예 : 지역 변수) “이름”을 포함한 환경도 캡슐화합니다.

이에 대한 자세한 내용은 C # 1, 2 및 3을 통해 사용자를 안내하는 클로저에 대한 기사를 참조하여 클로저 가 어떻게 일을 더 쉽게 만드는지 보여줍니다.


답변

C에서 함수 포인터는 함수에 대한 인수로 전달되고 함수의 값으로 반환 될 수 있지만 함수는 최상위 수준에만 존재합니다. 함수 정의를 서로 중첩 할 수 없습니다. C가 외부 함수의 변수에 액세스 할 수있는 중첩 함수를 지원하는 동시에 호출 스택의 위아래로 함수 포인터를 보낼 수있는 데 필요한 사항을 생각해보십시오. (이 설명을 따르려면 함수 호출이 C 및 대부분의 유사한 언어로 구현되는 방법에 대한 기본 사항을 알아야합니다 . Wikipedia 에서 호출 스택 항목을 찾아보십시오 .)

중첩 함수에 대한 포인터는 어떤 종류의 개체입니까? 코드의 주소가 될 수 없습니다. 호출하면 외부 함수의 변수에 어떻게 액세스 할 수 있습니까? (재귀 때문에 한 번에 활성화되는 외부 함수의 여러 다른 호출이있을 수 있음을 기억하십시오.) 이것을 funarg 문제 라고하며 , 두 가지 하위 문제가 있습니다 : 하향 funargs 문제와 상향 funargs 문제.

하향 funargs 문제, 즉 함수 포인터를 “스택 아래로”함수 포인터를 호출하는 함수에 대한 인수로 보내는 것은 실제로 C와 호환되지 않으며 GCC 중첩 함수를 하향 funarg로 지원합니다 . GCC에서 중첩 함수에 대한 포인터를 만들면 실제로 정적 링크 포인터 를 설정하는 동적으로 구성된 코드 조각 인 트램폴린에 대한 포인터를 얻은 다음 정적 링크 포인터를 사용하여 액세스하는 실제 함수를 호출합니다. 외부 함수의 변수.

위로 funargs 문제는 더 어렵습니다. GCC는 외부 함수가 더 이상 활성화되지 않은 경우 (호출 스택에 레코드가 없음) 트램폴린 포인터가 존재하도록 허용하지 않고 정적 링크 포인터가 쓰레기를 가리킬 수 있습니다. 활성화 레코드는 더 이상 스택에 할당 될 수 없습니다. 일반적인 해결책은 힙에 할당하고 중첩 된 함수를 나타내는 함수 개체가 외부 함수의 활성화 레코드를 가리 키도록하는 것입니다. 이러한 객체를 클로저 라고합니다 . 그런 다음 언어는 일반적으로 가비지 컬렉션 을 지원해야 하므로 레코드를 가리키는 포인터가 더 이상 없으면 레코드를 해제 할 수 있습니다.

Lambda ( 익명 함수 )는 실제로 별도의 문제이지만 일반적으로 익명 함수를 즉시 정의 할 수있는 언어를 사용하면 함수 값으로 반환 할 수 있으므로 결국 종료됩니다.


답변

람다는 동적으로 정의 된 익명 함수입니다. 클로저 (또는 둘의 설득력)에 관해서는 C에서 그렇게 할 수 없습니다. 전형적인 lisp 예제는 다음과 같은 라인을 따라 보일 것입니다.

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

C 용어로는의 어휘 환경 (스택)이 get-counter익명 함수에 의해 캡처되고 다음 예제와 같이 내부적으로 수정 된다고 말할 수 있습니다 .

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]>


답변

클로저는 즉석에서 미니 객체를 선언 할 수있는 것과 같이 함수 정의 지점의 일부 변수가 함수 논리와 함께 바인딩됨을 의미합니다.

C와 클로저의 한 가지 중요한 문제는 클로저가 가리키는 지 여부에 관계없이 스택에 할당 된 변수가 현재 범위를 벗어날 때 파괴된다는 것입니다. 이것은 사람들이 부주의하게 지역 변수에 대한 포인터를 반환 할 때 발생하는 종류의 버그로 이어질 것입니다. 클로저는 기본적으로 모든 관련 변수가 참조 계산되거나 힙에서 가비지 수집 된 항목임을 의미합니다.

모든 언어의 람다가 클로저인지 확실하지 않기 때문에 람다를 클로저와 동일시하는 것이 불편합니다. 때로는 람다가 변수 바인딩없이 로컬에서 정의 된 익명 함수라고 생각합니다 (Python 2.1 이전?).