요약
C++ Insights는 컴파일러가 몰래 끼워 넣는 코드를 C++ 소스 형태로 펼쳐 보여 주는 도구입니다.
안드레아스 페르티히가 만든 오픈소스 프로젝트입니다. Clang(LLVM 기반 C/C++ 컴파일러)으로 코드를 분석한 뒤, 평소에는 보이지 않는 변환 결과를 다시 C++ 코드로 써 줍니다.
왜 중요한가
- 컴파일러가 대신 써 주는 코드를 직접 보여 주므로, 문법 뒤에 숨은 동작을 추측하지 않고 확인할 수 있습니다.
- 웹에서 설치 없이 쓰고 결과를 링크로 공유할 수 있어, 학습과 토론의 공통 자료로 쓰입니다.
핵심 내용
- 컴파일러가 자동으로 만드는 생성자, 소멸자, 대입 연산자와 암시적 형 변환을 코드로 드러냅니다.
- 람다는 클래스로, 범위 기반 for 문은 일반 for 문으로 바꿔 보여 줍니다.
- auto와 decltype이 추론한 실제 타입, 구조적 바인딩의 풀어 쓴 형태도 보여 줍니다.
- 소스 대 소스 변환(코드를 받아 코드를 내놓는 방식)이지만, 결과가 항상 컴파일되지는 않는다고 밝혔습니다.
- C++11부터 C++23까지 지원하고 C++26 지원도 작업 중이며, 라이선스는 MIT입니다.
HN 반응
- 람다와 코루틴을 이 도구로 이해했다는 경험담이 이어졌고, 람다가 사실상 클래스라는 점에 놀라는 이용자도 있었습니다.
- 이런 도구가 필요할 만큼 C++이 복잡하다는 한탄에는, C의 -E나 Rust의 cargo expand처럼 다른 언어에도 비슷한 도구가 있다는 반론이 나왔습니다.
오, 저 이런 거 정말 좋아해요. 저는 C++ -> Clang -> JS 트랜스파일러를 만들어서, HTML로 C++ 소스 위에 값이 바뀌는 상태를 보여 주고 사람들이 상태 변화 전체를 한 단계씩 따라가 볼 수 있게 했어요. 입력값을 바꿔서 값이 어떻게 달라지는지도 볼 수 있는데, JS 계층에서 값을 다시 계산하기 때문이에요. 제가 굳이 트랜스파일까지 한 이유가 바로 이거고요. 아니었다면 상태 변화만 출력해서 CSV로 파싱해 단계별로 보여 주면 됐을 테니까요. 재미있는 프로젝트였어요!
https://youtu.be/ZPGysUX62OY
꼭 써 봐야겠네요. 특히 C++를 쓸 때면 “그래서 컴파일러는 지금 대체 뭘 생각하고 있는 거지?” 싶은 문제가 늘 있었거든요.
이 도구는 여기서 볼 수 있습니다 - https://cppinsights.io/
readme 예제에 람다 캡처가 나왔으면 좋았을 텐데요. 컴파일러 마법이 진짜로 드러나는 곳이 바로 거기거든요.
C++ 람다는
operator()를 오버로드한 구조체일 뿐입니다. 이걸 알고 나면 훨씬 덜 신비롭게 느껴지죠. 이런 걸 배우는 데 이 도구가 얼마나 유용한지 보여 주는 좋은 예이기도 합니다(물론 C++ 표준을 읽어도 되겠지만, 그럴 시간이 누구에게 있겠습니까?).예시는 이렇습니다: https://cppinsights.io/s/f04b6896
(이 예시는 C++의 범위 기반 for 루프가 어떻게 동작하는지도 보여 줍니다. 이것 역시 꽤 평범한 for 루프에 얹은 문법적 설탕(syntactic sugar)일 뿐이지만, 세부 사항이 흥미롭습니다. 일반적인 for 루프와 달리 루프 변수가 루프 본문 안에서 선언되거든요.)
C++에서는 그렇지 않은 것 아닌가요? 거기서는 이 코드가 컴파일되지 않습니다
( https://cppinsights.io/s/26405687 : error: use of undeclared identifier 'i')
https://en.cppreference.com/cpp/language/for :
*“for 문은 다음과 동등합니다:
단, […]라는 점은 다릅니다.”*
그리고
*“C에서는 init-statement와 condition의 스코프에서 선언된 이름을 statement의 스코프에서 가릴(shadow) 수 있지만, C++에서는 금지되어 있습니다:
“*
저는 람다 _캡처_를 보여 줬으면 좋겠다고 한 겁니다. 컴파일러가 스코프 경계를 넘나드는 변수가 무엇인지 자동으로 판단하고, 그 변수마다 코드를 끼워 넣는 과정이 들어 있으니까요.
여기 있는 도구에 샘플 코드를 직접 입력해 보면 볼 수 있습니다 - https://news.ycombinator.com/item?id=49928362
저도 같은 생각입니다.
https://news.ycombinator.com/item?id=49945197
람다가 그냥 클래스라고요!? 전혀 몰랐네요.
D에서 람다는 그냥 중첩 함수입니다:
이 도구가 마음에 들긴 하는데도, 슬픈 것 같기도 하고 화나고 짜증 나고 실망스러운 것 같기도 한, 뭐라고 딱 표현하기 어려운 묘한 기분이 드는 사람은 저뿐일까요. 언어에 이런 도구가 필요하다는 게(아니면 적어도 쓸모가 크다는 게) 말이에요.
제 코드를 처음 읽었을 때의 해석이 뭔지가 분명하게 드러났으면 좋겠다는 거예요. 고급 최적화나 컴파일러의 세세한 잔재주 얘기가 아니라 언어 자체의 얘기예요. 이 도구는 어떤 C++ 컴파일러에서든 쓸모가 있잖아요. 어쩌다 이렇게 복잡한 언어가 된 걸까요?
그러니까 제 말은(아주 개인적인 생각이라 죄송해요), 저한테 프로그래밍 언어는 문제와 그 해법을 생각하도록 돕는 도구예요. LLM 시대에는 더더욱 기계에게 뭔가를 시키는 도구가 아니라, 수학처럼 아이디어와 문제와 해법을 형식화하는 언어고요. 변호사가 아니면 이해할 수 없을 만큼 난해해서는 안 되잖아요, 그렇죠? 아니면 제가 너무 늙어서 생각이 완전히 엇나간 걸까요?
그럼 어떤 언어는 이런 도구가 필요 없다고 보시나요? 저는 인터프리터 언어나 JIT 언어를 쓸 때마다 이런 도구가 있었으면 싶거든요. 지금 일어나는 일이 정확히 어떤 의미론(semantics)에 대응되는지, 각 프런트엔드가 그걸 어떻게 최적화하는지 분명히 알고 싶어서요. JavaScript의
"a" + "b"같은 단순한 것도 그래요. 모든 JS 런타임이 이걸 어떻게 처리하는지 확실히 아세요? 파싱할 때 최적화로 문자열을 미리 합쳐 버릴까요? 더 큰 네이티브 문자열 타입을 미리 할당해 놓고 이어 붙일까요? 아니면 결국std::u16string("a") + std::u16string("b")와 같은 일을 할까요?아니요, 언어에 그 도구가 필요한 건 아닙니다. 언어를 처음 접하거나 특정 언어 기능을 처음 접하는 사람을 위한 교육용 도구입니다. 연습하다 보면 언어 기능은 저절로 몸에 배어서, 문제를 생각할 때 따로 의식하지 않게 되고요.
README 예제에서도 C++에 익숙한 사람은 POD 타입에 자동으로 만들어지는 복사 생성자 같은 건 신경 쓰지 않습니다. 대입이
operator=로 디슈가(desugar)된다는 사실이나, 대입에static_cast가 끼어들 수 있다는 사실도 마찬가지고요.물론 그 도구가 필요 없는 사람도 있죠. 요점은 다른 언어는 이런 설명 도구가 아예 필요 없고, 작은 책 한 권만 읽어도 충분하다는 거예요. 저는 C++도 정말로 이런 게 필요 없었으면 좋겠지만, 말씀드렸듯 아마 “저 혼자”만 그런가 봐요.
평범한 C에는 매크로를 펼쳐 주는
-E가 있습니다. Rust에는 cargo expand가, Haskell에는-ddump-simpl이, Common Lisp에는macroexpand가 있고요. 문법이 복잡하거나 사용자가 문법을 입맛대로 바꿀 수 있는 언어라면 어디든 이런 도구가 필요합니다. 그런 도구가 실제로 존재하느냐는 그 언어의 인기와 커뮤니티의 요구를 반영할 뿐입니다.정말 그런가요?
컴파일이 간단한 언어가 대체 어디 있나요? 컴파일 언어가 마음에 안 드시는 건가요? 뭘 불평하시는 건지 잘 모르겠네요.
Lisp는 컴파일하기 간단해요. 이 도구가 다루는 수준에서라면 C도 기본적인 편이고, 아마 다른 100개쯤 되는 언어도 그럴 거예요(다시 말하지만 컴파일러 전체가 아니라 이 도구가 보여 주는 부분만요).
컴파일 자체는 어렵지 않아요. 오류 복구를 잘 하고, 오류가 났을 때 좋은 피드백을 주고, 최적화까지 하는 게 어려운 거죠.
이 도구는 그런 부분은 알려 주지 않아요(그건 godbolt가 하죠). 이건 해석의 첫 번째 층이 어떻게 동작하는지에 대한 도구예요. 컴파일러를 만들려는 게 아니라, 코드가 실제로 무엇을 뜻하는지 이해하려는 거죠. 한눈에 봐서는 분명하지 않으니까요.
“C++는 지나치게 복잡하고, 움직이는 부분이 많고, 뒤에서 암묵적으로 일어나는 일이 너무 많아서 이해하기 어렵다”는 큰 취지에는 동의합니다. 다만 Lisp 환경에도 매크로 확장 스테퍼(macro expansion stepper)가 있는 데는 다 이유가 있다는 점만 잠깐 짚고 싶습니다.
고전 Lisp 1.5라면 그렇죠. 현대의 Lisp는 그렇지 않고, 그래서 Lisp In Small Pieces 같은 유명한 책들이 나온 겁니다.
C도 K&R C나 초기 C89/90 컴파일러, 그러니까 확장 기능이 폭발하고 새 언어 표준과 현대 ISA가 나오기 전 얘기라면 몰라도요.
언어와 도구를 모두 오해하고 계십니다.
모든 언어의 컴파일러에는 소스 언어의 추상화 수준을 높은 수준에서 낮은 수준으로 “낮추는(lowering)” 개념이 있습니다. 작성한 소스 -> 더 단순한 소스 -> AST -> IR -> 기계어 순이죠. 그중 하나가 “디슈가링(desugaring)”, 즉 복잡한 문법을 더 단순한 문법으로 바꿔서 이후 컴파일 단계가 더 작고 다루기 쉬운 언어 부분집합만 처리하게 하는 것입니다.
이 도구는 AST에서 소스를 다시 생성하는 방식으로, C++ 소스 언어 안에서 일어나는 디슈가링과 그 밖의 로워링(암시적 변환, 타입 추론 등)을 모두 보여 줍니다. 그래서 컴파일 과정의 해당 단계까지를 온전히 들여다볼 수 있습니다.
PS: 작성자가 직접 설명하는 영상 발표 - https://www.youtube.com/watch?v=VJ6ZvDRYzNE&t=558s
C++에만 국한된 얘기는 아니라고 봅니다. Python 데코레이터, Rust 매크로 등 거의 모든 언어의 컴파일러가 우리 눈에 보이지 않는 코드를 만들어 내니까요. C++는 그런 게 더 많고 예측하기도 더 어려울 뿐입니다. 확장된 버전을 볼 수 있다는 건 이런 일을 하는 어떤 언어에서든 유용합니다.
저도 비슷한 걸 만들어 봤는데, Clang AST로 복원한 코드가 여러 문제 때문에 컴파일이 안 되더라고요.
이런 문제들은 해결할 수 있을까요?
제가 보고 싶은 건 C++ 코드 블록마다(가능한 한 잘게 쪼개서) 컴파일에 시간이 얼마나 걸리는지예요.
정말 흥미롭네요.
저는 이 도구 덕분에 람다를 이해했어요. 정말 훌륭한 도구예요.
C++ 코루틴에도 좋아요.
C++20 코루틴은 .NET의 동작 방식과 awaitable 타입의 관련 매직 메서드에서 영감을 받은 것이라, .NET 모델을 알아 두면 도움이 됩니다.