C에서부터 겉핥기 해보는 메소드 디스패치(+ 함수 포인터) #118
kelly-chui
started this conversation in
Idea
Replies: 1 comment 2 replies
-
켈리 안녕하세요~ 궁금한 점 몇 가지 질문 드려요!
또 하나 이건 이야기해보고 싶은 부분이예요. |
Beta Was this translation helpful? Give feedback.
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Introduction
C++로 PS를 풀면 가끔 함수 포인터를 써야하는 경우가 생깁니다. Swift에서 클로저를 이용하는 것과 비슷한데, 주변 값을 캡처할 수 없다는 특징이 있어요. 함수 포인터는 C에도 존재하는 개념이어서 번뜩 이런 생각이 떠올랐습니다.
생각이 계속 확장되다 보니 Objective-C나 **C++**의 실제 클래스 구현도 이러지 않을까? 라는 생각이 들었습니다. 결국 vtable…캡처…1급 객체 이런 식으로 사고가 뻗어나가서 여러 개념들을 이을 수 있었어요.
이 글의 핵심 키워드는 다음과 같습니다:
What is a function pointer?
모든 것은 C에서 시작합니다. C에서 함수 포인터란 함수가 저장되어 있는 코드 영역의 주소를 가리키는 포인터에요. 여러분들은 모두 포인터 잘 다루시기 때문에 포인터에 대한 설명은 깊게 하지 않겠습니다.
아래는 변수 포인터
int * var_pointer
와 함수 포인터int (*func_pointer)
가 각각 가리키는 메모리 주소의 영역을 나타낸 그림입니다:OOP in C
C는 절차적(Procedural) 프로그래밍 언어라고 많이 표현되는데요. 과장이 아닌게 C에도 스트럭처가 존재하지만, 이 스트럭처는 단순히 여러 변수를 묶는 정도의 역할밖에 하지 못합니다. Swift 스트럭처와 구분하기 위에 아래부턴 C 스트럭처라고 하겠습니다.
하지만 포인터는 단순히 ‘메모리 주소 값’을 저장하는 변수이기 때문에 C 스트럭처에 함수 포인터를 포함할 수 있어요.
아래 코드는 자기 자신을 소개하는 함수인
intro_hgd
를 구현하고, 이 함수를 가리키는 포인터intro
를 멤버로 포함하는 C 스트럭처 입니다:me.intro(&me);
라인처럼 마치 C 스트럭처에 있는 메소드를 호출한 것 처럼 사용할 수 있게 되었습니다. 우리가 C++이나 Swift에서 쓰는 메소드와 다르게 약간 다른 점이 있다면 파라미터로 자기 자신을 넘겨줘야 하는데요. Python에서 본 것과 유사하죠?이는
intro_hgd
이 결국 외부 함수라서 자기 자신(self
)를 캡처하지 못해서 입니다. 우리는 그냥 함수 포인터로 외부 함수를 호출한 것 뿐이니까요.introducing a vtable
C++, Objective-C의 클래스도 C 스트럭처 기반으로 구현되었으며, 위의 방법과 유사한 방식으로 클래스와 메소드를 구현합니다. 가장 큰 다른점은 특히 C++은 클래스 내부에 있는 모든 메소드를 함수 포인터 배열의 형태로 관리합니다. 혹시 감이 오시나요? 이것이 바로 원시적인 vtable입니다.
아래 코드는 vtable을 통해서 함수 포인터를 관리하는 C 스트럭처를 구현한 코드입니다. 코드를 다 이해할 필요는 없어요. 그냥 ‘함수 포인터 배열을 가지고 있는 C 스트럭처’ 정도로만 이해해도 충분합니다:
위 코드는
vtable
에서 직접 함수 포인터를 꺼내오는 방식으로 메소드를 호출하는 방법을 사용합니다. 하지만 우리에게 친숙한 Swift, Python, C++ 등 객체 지향 언어들은 이 과정이 추상화 되어있어요. 즉, vtable 안에 있는 함수 포인터를 직접 꺼내 쓰지 않고, 런타임 혹은 컴파일 타임에 자동으로 함수 포인터를 찾아주는 방식으로 메소드 호출을 구현합니다. 이 과정을 디스패치라고 하고요.여기서 C++와 Objective-C의 방법이 나뉩니다:
vptr
을 씁니다, 객체 내부에vptr
이라는 포인터를 숨겨놓고,vtable
에서 해당 포인터가 가리키는 메소드의 주소를 가져오는 방식을 씁니다.selector
라는 고유한 식별자로 변환합니다. 이selector
를 통해서 런타임에 동적으로 함수 포인터를 찾아 호출하는 방식을 사용합니다.Method Dispatch in Objective-C
C++보다는 Objective-C의 방식에 더 포커싱을 맞춰볼게요. UIKit에서 버튼 액션 설정할 때 아직도
selector
를 썼던 경험이 다들 있으시죠?Objective-C는 C++스타일의 vtable을 사용하지 않습니다. 물론 ‘함수 포인터 배열’이라는 개념은 유지되요. Objective-C의 메소드 디스패치 과정을 설명하기 전에 최대한 간략화한 메소드 구조부터 보겠습니다:
Objective-C의 메소드는 하나의 C 스트럭처로 표현할 수 있습니다. 이 스트럭처에는
sel
,imp
두 개의 멤버 변수가 있는데, 각각 식별자와 함수 포인터입니다.다음은 아주 간략화한 객체의 구조입니다. 함수 포인터 배열을 볼 수 있어요:
이제 Objective-C의 디스패치 과정을 살펴봅시다.
sel
을 가진Method
객체를 찾는다.objc_msgSend
라고 해요.imp
에 저장되어있는 함수 포인터가 가리키는 함수 실행selector
의 역할을 이제 알겠죠?Why is a closure a first class object?
인과 관계가 약간 뒤바뀐 것일 수도 있지만, Swift에서 클로저가 1급 객체인 이유를 이제 대략 감을 잡았을 것 같아요. 위 글에서 이미 Objective-C에서 부터 메소드는 ‘단일 함수 포인터’가 아닌 C 스트럭처로 감싸진 하나의 ‘객체’ 인 것을 확인했습니다.
C 코드는 이제 그만 보고, Swift 코드를 하나 작성해보겠습니다. 많은 언어에서 캡처 개념을 익힐 때 예제로 사용하는 adder 코드에요:
makeAdder(_:)
함수는(Int) -> Int
타입 클로저를 리턴하는 함수입니다. 동시에total
,x
두 변수를 캡처해요.makeAdder(_:)
함수는 변수add5
에{ y in total += x + y; return total }
이라는 클로저를 할당하고 리턴됩니다.일반적으로 함수가 리턴(종료)되면, 메모리의 스택 영역에서 pop되면서
total
이나x
와 같은 로컬 변수들도 같이 해제되어야 하는데.add5
를 호출될 때 마다. 마치total
과x
가 어딘가에 저장되어 있는 것 처럼 동작합니다. 이게 바로 클로저의 캡처 덕분인건 알고 계시죠?그런데 여기서 한 가지 더 생각해볼 수 있습니다. 클로저는 왜 1급 객체일까요? 정확한 이유는 아니지만 그 힌트는 캡처된 컨텍스트의 존재에서 얻을 수 있습니다.
함수를 불러오기 위해선 함수 포인터를 사용해야 할 것이고, Objective-C의
selector
는 더 이상 필요하지 않습니다. 하지만 캡처한 컨텍스트를 저장할 프로퍼티(드디어 멤버 변수라는 말을 쓰지 않아도 되네요 ㅋㅋ)가 필요합니다.이 즈음에서 Swift에서 클로저 객체의 구현을 간략하게 추측해볼 수 있습니다(물론 실제 클로저 구현은 스트럭처로 되어있지 않습니다!):
ClosureObject
는 메모리의 주소값을 저장하고 있는functionPointer
와 힙에 있는 스트럭처(혹은 클래스)를 가리키는capturedContext
를 저장하고 있는 스트럭처입니다.functionPointer
는 함수 자체를 가리키기 때문에 코드영역,capturedContext
는 캡처한 환경을 가리키기때문에 힙 영역에 할당되어 있겠죠. 이런 방식으로 클로저가 1급 객체가 됩니다. 그냥 스트럭처니까 변수에 할당할 수 있어요!Dynamic Dispatch in Swift
Swift가 아무리 정적 타입 언어라고 하더라도, 결국엔
final
없이 서브클래싱을 하거나, 프로토콜로 실제 구현이 숨겨진 경우엔 다이나믹 디스패치를 해야합니다. 사실 제 생각에는 스태틱 디스패치만큼 다이나믹 디스패치를 하거나 혹은 더 많이 할수도 있어요.스위프트의 다이나믹 디스패치는 크게 2가지로 분류할 수 있습니다.
Witness Table을 간단하게 정리하면, 각 프로토콜을 컨펌하는 타입마다 생성되는 함수 포인터의 배열입니다. 각 타입마다 프토토콜의 요구사항을 다르게 구현했을 것으로 기대되기 때문에 따로 구분할 필요가 있어요. 여기부터는 너무 첫 발상(함수포인터로 C에서 OOP 구현하기)에서 뻗어나간 것 같아서 적당히 커트하겠습니다. 다음 기회가 있으면 그 때 작성할게요.
읽어주셔서 감사합니다~~
Beta Was this translation helpful? Give feedback.
All reactions