함수 포인터, 클로저 및 Lambda


86

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

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

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

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

감사.

답변:


108

람다 (또는 클로저 )는 함수 포인터와 변수를 모두 캡슐화합니다. 이것이 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이 범위에 포함되지 않은 다른 함수로 전달하려면 체인의 각 함수에 전달하거나 전역으로 승격하여 수동으로 유지해야합니다.

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

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


고마워요, 당신은 다른 사람들이 얻을 수있는 아이디어를 집으로 몰아 냈습니다.
없음

이 글을 작성할 때 이전 버전의 C를 사용했거나 함수를 전달하는 것을 기억하지 못했지만 이것을 테스트 할 때 언급 한 것과 동일한 동작을 관찰하지 못했습니다. ideone.com/JsDVBK
smac89 2011

@ smac89-lessThan 변수를 전역 변수로 만들었습니다. 대안으로 명시 적으로 언급했습니다.
Mark Brackett 2016

42

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

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

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

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

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

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

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

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


1
+1 설명과 OOP가 실제로 클로저라는 인용문- 기존 함수를 재사용하지만 새로운 자유 변수를 사용합니다 .-환경을 취하는 함수 (메서드) (새로운 상태 일 뿐인 객체 인스턴스 데이터에 대한 구조체 포인터) 작동합니다.
legends2k 2014-06-26

8

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'는 전역 범위에 있지 않더라도 액세스 할 수 있습니다.

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


귀하의 의견을 바탕으로 답변을 수정했습니다. 나는 여전히 용어의 세부 사항에 대해 100 % 명확하지 않아서 직접 인용했습니다. :)
Herms

익명 함수의 인라인 기능은 (대부분?) 주류 프로그래밍 언어의 구현 세부 사항입니다. 클로저에 대한 요구 사항은 아닙니다.
Mark Brackett

6

폐쇄 = 논리 + 환경.

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

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

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

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


void를 IEnumerable <Person>으로 바꾸는 것을 고려하십시오
Amy B

1
@David B : 건배, 끝났습니다. @edg : 변경 가능한 상태 이기 때문에 단순한 상태 이상이라고 생각합니다 . 즉, 로컬 변수를 변경하는 클로저를 실행하면 (메서드 내에있는 동안) 해당 로컬 변수도 변경됩니다. "환경"이이 사실을 더 잘 전달하는 것 같지만 털북숭이입니다.
Jon Skeet

나는 대답에 감사하지만 그것은 나를 위해 아무것도 명확하지 않으며 사람들은 단지 객체이고 당신이 그것에 대한 메소드를 호출하는 것처럼 보입니다. 아마도 C #을 모르는 것 같습니다.
없음

예, 메서드를 호출하고 있지만 전달하는 매개 변수는 클로저입니다.
Jon Skeet

4

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

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

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

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

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


3

람다는 동적으로 정의 된 익명 함수입니다. 클로저 (또는 둘의 설득력)에 관해서는 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]> 

2

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

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

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


2

GCC에서는 다음 매크로를 사용하여 람다 함수를 시뮬레이션 할 수 있습니다.

#define lambda(l_ret_type, l_arguments, l_body)       \
({                                                    \
    l_ret_type l_anonymous_functions_name l_arguments \
    l_body                                            \
    &l_anonymous_functions_name;                      \
})

출처의 예 :

qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]),
     lambda (int, (const void *a, const void *b),
             {
               dump ();
               printf ("Comparison %d: %d and %d\n",
                       ++ comparison, *(const int *) a, *(const int *) b);
               return *(const int *) a - *(const int *) b;
             }));

물론이 기술을 사용하면 응용 프로그램이 다른 컴파일러와 함께 작동 할 가능성이 제거되고 YMMV처럼 "정의되지 않은"동작이됩니다.


2

폐쇄 캡처 자유 변수를환경 . 주변 코드가 더 이상 활성화되지 않더라도 환경은 계속 존재합니다.

MAKE-ADDER새 클로저를 반환하는 Common Lisp의 예입니다 .

CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER

CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL

위의 기능 사용 :

CL-USER 55 > (let ((adder1 (make-adder 0 10))
                   (adder2 (make-adder 17 20)))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder1))
               (print (funcall adder1))
               (describe adder1)
               (describe adder2)
               (values))

10 
20 
30 
40 
37 
57 
77 
50 
60 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

참고한다는 DESCRIBE기능 쇼 기능 개체 모두 폐쇄는 동일하지만, 환경이 상이하다.

Common Lisp는 클로저와 순수 함수 객체 (환경이없는 객체)를 둘 다 함수로 만들고 여기에서는 FUNCALL.


1

주요 차이점은 C에서 어휘 범위 지정이 없기 때문입니다.

함수 포인터는 코드 블록에 대한 포인터입니다. 참조하는 스택이 아닌 변수는 전역, 정적 또는 유사합니다.

클로저, OTOH는 '외부 변수'또는 '상승 값'의 형태로 자체 상태를 갖습니다. 어휘 범위 지정을 사용하여 원하는대로 비공개 또는 공유 할 수 있습니다. 동일한 함수 코드로 많은 클로저를 만들 수 있지만 변수 인스턴스는 다릅니다.

일부 클로저는 일부 변수를 공유 할 수 있으므로 객체의 인터페이스가 될 수 있습니다 (OOP 의미에서). C에서 구조를 함수 포인터 테이블과 연관시켜야합니다 (이것이 C ++가 클래스 vtable과 수행하는 작업입니다).

요컨대, 클로저는 함수 포인터와 일부 상태입니다. 그것은 더 높은 수준의 구조입니다


2
WTF? C에는 확실히 어휘 범위가 있습니다.
Luís Oliveira

1
'정적 범위 지정'이 있습니다. 내가 이해하는 바와 같이 어휘 범위 지정은 동적으로 생성 된 함수가있는 언어에서 유사한 의미를 유지하는 더 복잡한 기능이며이를 클로저라고합니다.
Javier

1

대부분의 응답은 클로저에 익명 함수에 대한 함수 포인터가 필요하지만 Mark가 작성한 것처럼 클로저는 명명 된 함수와 함께 존재할 수 있음을 나타냅니다 . 다음은 Perl의 예입니다.

{
    my $count;
    sub increment { return $count++ }
}

클로저는 $count변수 를 정의하는 환경입니다 . increment서브 루틴 에서만 사용할 수 있으며 호출간에 지속됩니다.


0

C에서 함수 포인터는 함수를 역 참조 할 때 함수를 호출하는 포인터이고, 클로저는 함수의 논리와 환경 (변수 및 변수가 바인딩 된 값)을 포함하는 값이며 람다는 일반적으로 다음 값을 참조합니다. 실제로 이름이 지정되지 않은 함수입니다. C에서 함수는 일급 값이 아니므로 전달할 수 없으므로 대신 포인터를 전달해야하지만 함수 언어 (Schema와 같은)에서는 다른 값을 전달하는 것과 동일한 방식으로 함수를 전달할 수 있습니다.

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