C 언어 표준에는 없지만 GCC가 오랫동안 확장 기능으로 제공해 온 '중첩 함수(nested function)'는 이름 그대로 함수 안에 또 다른 함수를 정의하는 문법이다. 커널 개발자이자 표준화 논의에 참여해 온 마르틴 우에커가 자신의 블로그에서 이 기능의 내부 구현을 설명했다. 함수 포인터를 얻을 때 필요한 트램펄린(trampoline) 문제는 이전 글에서 다뤘으므로 이번에는 중첩 함수가 부모 함수의 지역 변수에 어떻게 접근하는지, 그 기본 메커니즘에 초점을 맞춘다. 실무적으로 이 이야기가 흥미로운 이유는, 결론적으로 이 구현이 C++ 람다와 본질적으로 크게 다르지 않기 때문이다.
변수를 캡처하지 않는 경우
가장 단순한 경우는 중첩 함수가 부모의 어떤 변수도 건드리지 않을 때다. 이때 컴파일러는 그 함수를 부모 밖으로 그대로 끄집어내(lift) 독립된 별도 함수로 컴파일할 수 있다. 이런 함수도 작은 보조 함수를 정의하거나, 지역적으로 선언한 타입을 그 함수 안에서 쓰고 싶을 때 유용하다. 실제로 C 표준화 그룹인 WG14는 static 저장 수명으로 정의한 이런 '캡처 없는 지역 함수'를 허용하는 제안 N3884를 검토하고 있다. 즉 표준 C에서도 제한된 형태의 지역 함수를 정식으로 받아들일 가능성이 열려 있다는 뜻이다.
부모 변수에 접근할 때
문제는 중첩 함수가 부모의 변수, 예컨대 k를 참조할 때다. 실행 시점에 중첩 함수는 부모 함수의 그 변수를 찾아낼 수 있어야 한다. 전통적인 방식은 부모의 스택 프레임을 가리키는 포인터를 넘겨주고, 정해진 스택 슬롯에서 변수를 읽는 것이었다. PASCAL 같은 언어가 이 기법을 썼고, x86에는 이를 지원하는 enter·leave 전용 명령어까지 존재한다. 그러나 오늘날 GCC는 이 방식을 쓰지 않는다.
GCC는 중간 표현 단계의 이른 시점에 중첩 함수를 '낮추는(lowering)' 패스를 수행한다. 이 과정에서 자식 함수가 접근하는 부모의 변수들을 하나의 합성 구조체로 모으고, 그 구조체를 가리키는 포인터를 숨겨진 인자로 중첩 함수에 넘긴다. 변수 접근은 모두 해당 구조체 멤버에 대한 접근으로 다시 쓰인다. 이 방식의 장점은 명확하다. 중첩 함수라는 기능이 컴파일러 나머지 부분과 분리된다는 점이다. 컴파일러 입장에서 그것은 그저 일반 구조체를 가리키는 추가 인자일 뿐이므로, 자식이 참조하지 않는 부모 변수는 전혀 영향을 받지 않고, 이 프레임 구조체 자체도 프로그램의 여느 구조체처럼 최적화된다. 그래서 단순한 예제는 중첩 함수라는 개념을 전혀 모르는 일반 최적화 코드에 의해 덧셈 한 줄로 축약되기도 한다. 중첩이 여러 단계일 때는 구조체가 한 단계 위 프레임 구조체로의 링크를 담아 사슬 형태를 이루지만, 실무에서 이런 다단계 중첩이 필요한 경우는 드물다.
C++ 람다와의 거리
GCC 중첩 함수를 C++ 람다와 비교하면 언어 표면의 차이가 먼저 눈에 띈다. 람다는 이름이 없는 함수 리터럴이자 표현식인 반면, GCC 중첩 함수는 중첩된 문맥에 등장하는 평범한 함수 정의다. 또 하나, GCC 중첩 함수의 타입은 일반적인 함수 타입이지만, C++ 람다의 타입은 이름 붙일 수 없는 고유한 익명 타입, 이른바 '볼드모트 타입'이다. 그러나 구현 관점에서 이 둘은 근본적 차이가 아니다. 실제로 앞의 예제는 람다 객체를 써서 그대로 C++로 옮길 수 있으며, C++ 컴파일러 역시 람다를 캡처한 변수의 복사본이나 참조를 담은 구조체(호출 가능 객체)로 변환한다. 캡처 변수를 구조체에 모아 넘긴다는 뼈대는 사실상 같다.
남는 차이는 한 부모 안에 중첩 함수가 둘 있을 때 드러난다. GCC는 k를 담은 프레임 구조체를 부모에 하나만 만들고 두 중첩 함수 모두에게 똑같은 포인터를 넘겨, 하나의 공유 환경을 쓰게 한다. 반면 C++ 컴파일러는 람다 표현식마다 별도 객체를 만들되 각 객체가 스택 위의 같은 k를 참조하도록 한다. 구현은 다르지만 두 언어 버전의 의미는 정확히 동일하다.
정리하면 GCC 중첩 함수는 C++ 람다의 좁은 의미론적 부분집합에 해당하고, 역사적으로는 서로 다른 접근에서 출발했지만 구현의 골격은 크게 다르지 않다. 저자의 지적처럼, 이미 C++ 람다를 구현한 컴파일러라면 기존 람다 지원을 재활용해 GCC 중첩 함수와 같은 문법·의미의 기능을 어렵지 않게 노출할 수 있다. C에 지역 함수를 도입하려는 표준화 논의를 지켜보는 개발자라면, '새 기능'처럼 보이는 것이 실제로는 이미 검증된 람다 인프라 위에 얹힐 수 있는 성질임을 염두에 둘 만하다.
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공