IT이야기

함수 포인터, 폐쇄 및 람다

cyworld 2022. 6. 29. 21:12
반응형

함수 포인터, 폐쇄 및 람다

저는 지금 기능 포인터에 대해 배우고 있는데, 이 주제에 대한 K&R 장을 읽고 있을 때 가장 먼저 떠오른 것은 "야, 이건 마치 폐막 같다"는 것이었습니다.나는 이 가정이 근본적으로 틀렸다는 것을 알고 있었고 온라인에서 검색한 결과 이 비교에 대한 어떠한 분석도 찾을 수 없었다.

그렇다면 C-스타일 함수 포인터는 폐쇄나 람다와 근본적으로 다른 이유는 무엇일까요?내가 아는 한, 함수의 포인터가 함수를 익명으로 정의하는 것과 달리 정의된(이름 붙여진) 함수를 가리키고 있다는 사실과 관련이 있다.

왜 함수로의 전달은 이름이 없는 두 번째 경우보다 더 강력한 것으로 보이는가? 첫 번째 경우에서는 그냥 전달되는 일반적인 일상적인 함수일 뿐입니다.

그 둘을 그렇게 가까이서 비교하는 것이 어떻게 그리고 왜 틀렸는지 알려주세요.

고마워요.

'진짜' 폐쇄가 있든 없든 언어 컴파일러를 쓴 사람으로서, 저는 위의 답변 중 몇 가지에 정중히 동의하지 않습니다.Lisp, Scheme, ML 또는 Haskell 폐쇄는 동적으로 새로운 기능을 생성하지 않습니다.대신 기존 함수를 재사용하지만 새로운 자유 변수를 사용하여 재사용합니다.자유 변수의 집합은 적어도 프로그래밍 언어 이론가들에 의해 종종 환경이라고 불립니다.

폐쇄는 기능과 환경을 포함하는 집합체일 뿐입니다.뉴저지 컴파일러의 Standard ML에서는 하나의 필드를 레코드로 표현했습니다.한 필드는 코드에 대한 포인터를 포함하고 다른 필드는 자유 변수의 값을 포함하고 있습니다.컴파일러는 포인터를 포함한 새로운 레코드를 같은 코드에 할당하여 동적으로 새로운 클로저(함수가 아님) 작성했지만 자유변수에 대해서는 다른 을 사용했습니다.

이 모든 것을 C로 시뮬레이트 할 수 있지만, 그것은 골칫거리입니다.다음 두 가지 기술이 사용됩니다.

  1. 함수(코드)에 포인터를 전달하고 자유 변수에 별도의 포인터를 전달하여 닫힘을 두 개의 C 변수로 분할합니다.

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

기술 #1은 C에서 어떤 다형성을 시뮬레이트하려고 할 때 이상적입니다.환경의 유형을 밝히고 싶지 않을 때는 void* 포인터를 사용하여 환경을 나타냅니다.예를 들어 Dave Hanson의 C Interfaces and Implementations를 참조하십시오.기능 언어의 네이티브 코드 컴파일러에서 일어나는 것과 더 유사한 기법 #2는 또 다른 익숙한 기법과도 유사합니다.가상 멤버 함수가 있는 C++ 객체.구현은 거의 동일합니다.

이러한 관찰은 헨리 베이커의 지혜로 이어졌습니다.

Algol/Fortran 세계의 사람들은 향후 효율적인 프로그래밍에서 기능 폐쇄가 어떤 용도로 사용될 수 있는지 이해할 수 없다고 수년간 불평했습니다.그 후 '객체 지향 프로그래밍' 혁명이 일어났고, 이제는 모든 사람들이 함수 폐쇄를 사용하여 프로그래밍하지만, 여전히 그렇게 부르는 것을 거부한다.

람다(또는 폐쇄)는 함수 포인터와 변수를 모두 캡슐화합니다.따라서 C#에서는 다음 작업을 수행할 수 있습니다.

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

나는 거기서 익명의 대리인을 클로징으로 사용했다(그 구문은 람다 등가물보다 조금 명확하고 C에 가깝다).그것은 덜 캡처되었다.(스택 변수)를 폐쇄에 입력합니다.닫힘이 평가될 때 less Than(스택 프레임이 파괴되었을 수 있음)이 계속 참조됩니다.내가 덜 바꾸면그런 다음 비교를 변경합니다.

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;
}

하지만 이제 평가할 때 두 가지 주장을 통과해야 합니다.이 함수 포인터를 보다 적은 다른 함수에 전달하고 싶은 경우범위외의 경우는, 체인의 각 기능에 전달하거나, 글로벌하게 프로모트 하거나 둘 중 하나입니다.

폐쇄를 지원하는 대부분의 주류 언어는 익명 기능을 사용하지만, 이에 대한 요구사항은 없습니다.익명 함수가 없는 폐쇄와 폐쇄가 없는 익명 함수가 있을 수 있습니다.

요약: 닫힘은 함수 포인터와 캡처된 변수의 조합입니다.

대부분의 반응은 폐쇄가 함수 포인터(아마도 익명 함수에 대한)를 필요로 한다는 것을 나타내지만 Mark가 쓴 처럼 폐쇄는 명명된 함수와 함께 존재할 수 있습니다.Perl의 예를 다음에 나타냅니다.

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

이란 ''를 입니다.$count★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★increment서브루틴과 콜 간에 지속됩니다.

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는 분명 "정의되지 않은" 동작입니다.

C에서 함수 포인터는 함수에 대한 인수로 전달되고 함수의 값으로 반환될 수 있지만 함수는 상위 레벨에만 존재합니다. 함수 정의를 서로 중첩할 수 없습니다.외부 함수의 변수에 액세스 할 수 있는 네스트된 함수를 C가 지원하면서 콜스택의 위아래로 함수 포인터를 송신할 수 있는 경우에 대해 생각해 봅시다(이 설명을 따르려면 C 및 대부분의 유사한 언어로 함수콜이 실장되는 방법에 대한 기본을 알아야 합니다. 스타를 참조합니다.ck 엔트리를 Wikipedia에서 참조).

중첩된 함수에 대한 포인터는 어떤 개체입니까?코드 주소일 수 없습니다.왜냐하면 코드를 호출하면 외부 함수의 변수에 어떻게 접근할 수 있을까요?(재귀로 인해 외부 함수의 여러 콜이 동시에 활성화 될 수 있습니다).이것을 푸나그 문제라고 부릅니다.하향 푸나그 문제와 상향 푸나그 문제라는 두 가지 하위 문제가 있습니다.

하위 펑스의 문제, 즉 호출하는 함수에 대한 인수로서 함수 포인터를 "down the stack"으로 보내는 문제는 실제로는 C와 호환되지 않으며, GCC는 하위 펑스의 중첩된 함수를 지원합니다.GCC에서는 중첩된 함수에 대한 포인터를 작성하면 트램펄린에 대한 포인터를 얻을 수 있습니다.트램펄린은 동적으로 구성된 코드 조각으로 정적 링크 포인터를 설정한 후 실제 함수를 호출합니다.이 코드는 정적 링크 포인터를 사용하여 외부 함수의 변수에 액세스합니다.

상향 펑스 문제는 더 어렵다.외부 함수가 활성화되지 않고(콜스택에 레코드가 없는 경우), 스태틱링크 포인터가 가비지를 가리킬 가능성이 있는 경우, GCC는 트램펄린 포인터가 존재하는 것을 막을 수 없습니다.액티베이션레코드를 스택에 할당할 수 없게 되었습니다.일반적인 해결책은 힙에 할당하고 중첩된 함수를 나타내는 함수 객체가 외부 함수의 액티베이션레코드를 가리키도록 하는 것입니다.이러한 개체를 폐쇄라고 합니다.그러면 일반적으로 해당 언어를 사용하여 가비지 컬렉션을 지원해야 하므로 레코드를 가리키는 포인터가 더 이상 없으면 레코드를 해방할 수 있습니다.

람다(익명의 함수)는 실제로는 별개의 문제이지만, 일반적으로 익명 함수를 즉석에서 정의할 수 있는 언어를 사용하면 함수의 값으로 반환할 수 있으므로 닫힘이 됩니다.

람다는 동적으로 정의된 익명 함수입니다.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 이전?).

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

Lisp의이며 Common Lisp는 다음과 같습니다.MAKE-ADDER을 사용하다

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.

C에서는 함수를 인라인으로 정의할 수 없으므로 닫힘을 실제로 만들 수 없습니다.미리 정의된 방법에 대한 참조를 전달하기만 하면 됩니다.익명 메서드/클로저를 지원하는 언어에서는 메서드의 정의가 훨씬 더 유연합니다.

간단히 말하면, 함수 포인터에는 (글로벌 스코프를 카운트하지 않는 한) 관련된 스코프가 없는 반면, 클로저에는 이를 정의하는 메서드의 스코프가 포함됩니다.람다를 사용하면 메서드를 작성하는 메서드를 작성할 수 있습니다.폐쇄를 통해 "일부 인수를 함수에 묶고 결과적으로 하위 함수를 얻을 수 있습니다."(Thomas의 설명에서 인용).C에서는 할 수 없어요.

편집: 예제를 추가합니다(Actionscript와 유사한 구문을 사용합니다.그것이 바로 제가 지금 생각하고 있는 것입니다).

다른 메서드를 인수로 사용하지만 메서드가 호출되었을 때 해당 메서드에 파라미터를 전달하는 방법을 제공하지 않는 메서드가 있다고 가정해 보십시오.예를 들어, 전달한 메서드를 실행하기 전에 지연을 일으키는 메서드(stupid한 예지만 단순하게 하고 싶다) 등이 있습니다.

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' 및 'object Processor'에 액세스할 수 있습니다(글로벌 범위에는 포함되지 않지만).

그게 말이 됐길 바라.

폐쇄 = 논리 + 환경.

예를 들어 다음 C#3 방식을 생각해 보겠습니다.

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

lamda 표현식은 로직("이름 비교")뿐만 아니라 매개 변수(예: 로컬 변수) "이름"을 포함한 환경도 캡슐화합니다.

이에 대한 자세한 내용은 C# 1, 2, 3을 안내하는 폐쇄에 대한기사를 보시기 바랍니다. 폐쇄가 어떻게 상황을 쉽게 만드는지 보여줍니다.

주된 차이는 C의 어휘 범위가 부족하기 때문에 발생한다.

함수 포인터는 코드 블록에 대한 포인터입니다.참조하는 비스택 변수는 글로벌 변수, 스태틱 변수 또는 유사 변수입니다.

폐쇄(OTOH)는 '외부 변수' 또는 '업값' 형태로 자체 상태를 가집니다.어휘 범위를 사용하여 원하는 만큼 비공개 또는 공유할 수 있습니다.기능 코드는 같지만 변수 인스턴스(instance)가 다른 많은 폐쇄를 만들 수 있습니다.

몇 개의 폐쇄가 일부 변수를 공유할 수 있으며, 객체 인터페이스(OOP 의미)도 마찬가지일 수 있습니다.이를 C에서 작성하려면 구조체를 함수 포인터의 테이블과 관련지어야 합니다(C++가 클래스 vtable과 함께 하는 것).

간단히 말해서, 폐쇄는 함수 포인터 + 일부 상태입니다.더 높은 수준의 구성입니다.

C에서 함수 포인터는 함수를 참조할 때 호출하는 포인터이며, 닫힘은 함수의 로직과 환경(변수와 변수들이 바인딩된 값)을 포함하는 값이며, 람다는 일반적으로 이름이 지정되지 않은 함수인 값을 참조합니다.C에서는 함수는 퍼스트 클래스 값이 아니기 때문에 전달할 수 없으므로 포인터를 대신 전달해야 합니다.그러나 기능 언어(Scheme 등)에서는 다른 값을 전달하는 것과 같은 방법으로 함수를 전달할 수 있습니다.

언급URL : https://stackoverflow.com/questions/208835/function-pointers-closures-and-lambda

반응형