트레이트의 async 함수에서 동적 디스패치를 처리하는 다양한 전략을 비교하고, 기본 전략으로 박싱을 강제하지 않으면서도 명시성과 성능을 균형 있게 달성하는 방법을 모색한다. Boxing 어댑터 제안, 설계 원칙(투명성·생산성·성능) 평가, 그리고 향후 ABI 확장 가능성까지 논의한다.
지난 몇 달 동안 Tyler Mandry와 저는 트레이트에서의 async 함수 설계를 담은 현재 제안, 즉 “미래에서 온 사용자 가이드”를 공유해 왔습니다. 이 글에서는 그 제안의 한 측면, 즉 동적 디스패치를 어떻게 처리할지 깊이 파고들어 보려 합니다. 여기서의 목표는 선택지의 공간을 조금 탐색하고, 특히 까다로운 주제 하나—할당 가능성에 대해 얼마나 명시적이어야 하는가?—를 다루는 것입니다. 이 주제는 어렵고, 궁극적으로 이렇게 묻게 만듭니다. 러스트의 혼은 무엇인가?
이 글 전반에서 아래의 예제 트레이트 AsyncIterator에만 집중하겠습니다:
trait AsyncIterator {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}
그리고 특히 동적 디스패치를 통해 next를 호출하는 상황에 초점을 맞춥니다:
fn make_dyn<AI: AsyncIterator>(ai: AI) {
use_dyn(&mut ai); // <— `&mut AI`에서 `&mut dyn AsyncIterator`로의 강제 변환
}
fn use_dyn(di: &mut dyn AsyncIterator) {
di.next().await; // <— 바로 이 호출!
}
이 글에서는 이 코드 조각에 집중하지만, 여기서 하는 이야기는 impl Trait을 반환하는 메서드를 가진 어떤 트레이트에도 적용됩니다(참고로 async 함수는 impl Future를 반환하는 함수의 축약형입니다).
우리가 직면하는 기본적인 도전은 다음과 같습니다:
use_dyn은 dyn 뒤에 어떤 impl이 있는지 모르기 때문에, 모든 경우에 동작할 고정 크기의 공간을 할당해야 합니다. 또한 어떤 poll 메서드를 호출할지 알 수 있도록 vtable도 필요합니다.AI::next는 자신의 next 함수가 반환하는 future를 호출자의 기대에 맞게 포장할 수 있어야 합니다.이 문제를 더 자세히 설명한 내용은 이 시리즈의 첫 번째 글1에 있습니다.
여기서의 어려움 중 하나는 이 문제를 풀 수 있는 방법이 매우 많고, 그 어느 것도 “명백한 최선”으로 보이지 않는다는 점입니다. 아래는 이 상황을 다루는 다양한 방법의—제가 보기엔 포괄적인—목록입니다. 목록에 없는 아이디어가 있다면 꼭 듣고 싶습니다.
박싱(Box it). 가장 눈에 띄는 전략은 피호출자가 future 타입을 박싱하여 사실상 Box<dyn Future>를 반환하게 하고, 호출자는 가상 디스패치를 통해 poll 메서드를 호출하는 것입니다. 이는 async-trait 크레이트가 하는 방식과 같습니다(다만 그 크레이트는 정적 디스패치에도 박싱을 하며, 우리가 꼭 그럴 필요는 없습니다).
커스텀 할당자로 박싱. future를 박싱하되, 커스텀 할당자를 사용하고 싶을 수 있습니다.
박싱하되 호출자에서 박스를 캐시. 대부분의 경우 박싱 자체는 성능 문제를 일으키지 않습니다. 단, 타이트 루프 안에서 반복될 때는 예외입니다. Mathias Einwag가 지적했듯, 동일한 객체에 대해 next를 반복 호출하는 코드라면, 호출자가 호출 사이에 박스를 캐시해 두고 피호출자가 이를 재사용하게 할 수 있습니다. 이렇게 하면 실제 할당은 한 번만 수행됩니다.
이터레이터에 인라인하기. 또 다른 옵션은 함수에 필요한 모든 상태를 AsyncIter 타입 자체에 저장하는 것입니다. 이는 사실 기존의 Stream 트레이트가 하는 방식이기도 합니다. 생각해 보면, future를 반환하는 대신 poll_next 메서드를 제공하여, Stream의 구현체가 사실상 future 그 자체가 되고, 호출자는 아무 상태도 저장할 필요가 없습니다. Tyler와 저는 사용자 개입 없이 더 일반적인 인라이닝 방법을 고안했는데, AsyncIterator 타입을 W라는 다른 타입으로 감싸고, 그 안에 next future를 저장하기에 충분한 크기의 필드를 두는 방식입니다. next를 호출하면 이 래퍼 W는 future를 그 필드에 저장한 후, 그 필드에 대한 포인터를 반환하여 호출자가 그 포인터만 poll하면 됩니다. 단, 이 방식의 문제는 인라이닝이 &mut self 메서드에나 잘 맞는다는 점입니다. 그 경우 동시에 활성화된 future가 최대 한 개뿐이기 때문입니다. 반면 &self 메서드에서는 활성 future가 얼마든지 많을 수 있습니다.
박싱하되 피호출자에서 박스를 캐시. 전체 future를 AsyncIterator 타입에 인라인하는 대신, 포인터 한 워드 정도의 슬롯만 인라인하여 next가 반환하는 Box를 캐시·재사용할 수 있습니다. 이 전략의 장점은 캐시된 박스가 이터레이터와 함께 이동하며, 호출자를 가로질러 재사용될 수 있다는 것입니다. 단점은 호출자가 끝난 뒤에도, 객체가 파괴될 때까지 캐시된 박스가 살아 있다는 점입니다.
호출자가 최대 크기만큼 할당. 또 다른 전략은 호출자가 스택에 큰 덩어리의 공간을 할당하는 것입니다. 모든 피호출자에게 충분히 큰 공간을 말이죠. 코드가 처리할 피호출자들을 알고 있고, 그 future들의 크기가 서로 충분히 비슷하다면 이 전략은 잘 맞습니다. Eric Holk가 최근 출시한 [stackfuture crate]가 이를 자동화하는 데 도움이 됩니다. 이 전략의 문제는 호출자가 모든 피호출자의 크기를 알아야 한다는 것입니다.
호출자가 일정 공간을 할당하고, 큰 피호출자에 대해서는 박싱으로 폴백. 모든 피호출자의 크기를 알 수 없거나 그 분포가 넓다면, 호출자가 일정량의 스택 공간(예: 128바이트)을 할당하고, 그 공간이 충분치 않으면 피호출자가 Box를 사용하게 하는 전략도 있습니다.
호출자 측 alloca. vtable에 반환될 future의 크기를 저장해 두고, 호출자가 동적으로 그만큼 “alloca”—즉 스택 포인터를 그만큼 증가—하려는 생각을 할 수 있습니다. 흥미롭게도, 이는 러스트의 async 모델과 맞지 않습니다. async 태스크는 스택 프레임의 크기가 사전에 알려져 있어야 합니다.
사이드 스택. 앞선 제안과 유사하게, 각 태스크에 대해 런타임이 일종의 “동적 사이드 스택”을 제공한다고 상상해 볼 수 있습니다.2 그러면 이 스택에 필요한 만큼의 공간을 할당할 수 있습니다. 아마도 이게 가장 효율적일 수 있지만, 런타임이 동적 스택을 제공할 수 있다는 가정을 필요로 합니다. embassy 같은 런타임은 이를 제공할 수 없습니다. 또한 지금으로서는 이런 것을 위한 프로토콜도 없습니다. 사이드 스택을 도입하면 러스트 async 모델의 매력—미리 “정확한 크기의 스택”을 할당해 태스크마다 “큰 스택”을 할당하지 않도록 설계했다는 점3—을 일부 갉아먹는 면도 있습니다.
트레이트의 async 함수에 관한 초기 목표 중 하나는 가능한 한 “자연스럽게” 느껴지게 하는 것이었습니다. 특히 동적 디스패치와 함께 사용할 때도 동기 함수와 똑같이 쓰길 바랐습니다. 다시 말해 다음 코드가 컴파일되길 바랐고, use_dyn이 다른 크레이트로 옮겨져서(따라서 호출자가 누군지 모른 채로) 컴파일되어도 동작하길 원했습니다:
fn make_dyn<AI: AsyncIterator>(ai: AI) {
use_dyn(&mut ai);
}
fn use_dyn(di: &mut dyn AsyncIterator) {
di.next().await;
}
제 희망은, 대부분의 경우에 잘 동작하는 어떤 기본 전략을 선택해 이 코드가 그대로 동작하게 만들고, 그 기본 전략이 잘 맞지 않는 코드에 대해서는 다른 전략을 고를 수 있는 방법을 제공하는 것이었습니다. 문제는 “거의 항상 자명하고 옳은” 단일 기본 전략이 보이지 않는다는 데 있습니다…
| 전략 | 단점 |
|---|---|
| 기본 할당자로 박싱 | 할당이 필요, 그리 효율적이지 않음 |
| 호출자 측 캐시 + 박싱 | 할당이 필요 |
| 이터레이터에 인라인 | AI에 공간 추가, &self에는 잘 맞지 않음 |
| 피호출자 측 캐시 + 박싱 | 할당이 필요, AI에 공간 추가, &self에는 잘 맞지 않음 |
| 최대 크기만큼 할당 | 크레이트를 가로질러서는 쓰기 어려움, 광범위한 프로시저 간 분석 필요 |
| 일부 공간 + 폴백 | 할당자 사용, 광범위한 프로시저 간 분석 또는 임의의 추측 필요 |
| 호출자 측 alloca | async 러스트와 비호환 |
| 사이드 스택 | 런타임 협력과 할당 필요 |
여기서 “러스트의 혼”에 이릅니다. 위의 표를 볼 때, “자명하게 옳은” 것에 가장 가까운 전략은 “박싱”으로 보입니다. 분리 컴파일과 잘 맞고, 러스트의 async 모델과도 잘 어우러지며, 사람들이 오늘날 실제로 하고 있는 방식과도 일치합니다. 프로덕션에서 async 러스트를 쓰는 많은 사람들과 이야기해 보았고, 거의 모두가 “기본은 박싱, 하지만 제어할 수 있게”가 실무에서 아주 잘 통할 것이라고 동의했습니다.
그럼에도, 이 방식을 기본으로 삼자는 아이디어를 내놓았을 때 Josh Triplett은 강하게 반대했는데, 그럴 만한 이유가 있다고 봅니다. Josh의 핵심 우려는 이것이 러스트의 선을 넘는다는 데 있었습니다. 지금까지는 어떤 식으로든 명시적인 연산(함수 호출일 수 있음) 없이 힙 메모리를 할당할 방법이 없었습니다. 하지만 “박싱”을 기본 전략으로 삼고 싶다면, 표면적으로 “순진해 보이는” 러스트 코드를 작성해도 실제로는 Box::new를 호출하게 됩니다. 특히 next가 호출될 때마다 future를 박싱하기 위해 Box::new를 호출하게 되는데, make_dyn과 use_dyn만 훑어봐서는 그 사실을 알아차리기 어렵습니다.
이 점이 문제가 될 수 있는 예로, 할당은 항상 큰 주의를 기울여서만 하는 민감한 시스템 코드를 작성한다고 해 봅시다. 그 코드가 no-std는 아니고, 할당자에 접근할 수는 있지만 그래도 어디에서 할당이 일어나는지 정확히 알고 싶습니다. 오늘날은 수작업으로 감사를 할 수 있습니다. Box::new나 vec![] 같은 “명백한” 할당 지점을 스캔하면 됩니다. 하지만 이 제안하에서는 여전히 가능 하긴 하지만, 코드에 할당이 존재한다는 사실이 훨씬 덜 명백해집니다. 할당이 vtable 구성 과정의 일부로 “주입”되기 때문입니다. 이것을 알아내려면 러스트의 규칙을 상당히 잘 알아야 하고, 또한 피호출자의 시그니처도 알아야 합니다(이 경우 vtable은 암시적 강제 변환의 일부로 만들어지기 때문). 요컨대, 할당을 스캔하는 작업은 비교적 명백한 것에서 러스트학 박사 학위를 요구하는 일로 바뀝니다. 흠.
다른 한편으로, 만약 중요한 것이 “할당 스캔”이라면, 다양한 방식으로 이를 개선할 수 있습니다. 예를 들어 “기본 허용” 린트를 추가해, “기본 vtable”이 구성되는 지점을 표시하고 프로젝트에서 이를 활성화할 수 있습니다. 이렇게 하면 컴파일러가 미래에 할당이 발생할 수 있는 지점을 경고할 것입니다. 사실, 오늘날에도 할당 스캔은 제가 묘사한 것보다 훨씬 어렵습니다. 자신의 함수가 할당하는지는 쉽게 볼 수 있지만, 그 함수의 피호출자들이 무엇을 하는지는 쉽게 볼 수 없습니다. 모든 의존성을 깊이 읽어야 하고, 함수 포인터나 dyn Trait 값이 있다면 어떤 코드가 호출될 수 있는지도 파악해야 합니다. 컴파일러/언어 차원의 지원이 있다면, 이 과정을 훨씬 일급으로, 더 나은 방식으로 만들 수 있습니다.
어떤 면에서는 기술적 논쟁은 본질과 다를 수도 있습니다. “러스트는 할당을 명시적으로 만든다”는 점이 러스트 설계의 핵심 속성으로 널리 받아들여져 왔습니다. 이 변경을 통해 우리는 그 규칙을 “러스트는 대부분의 경우 할당을 명시적으로 만든다” 정도로 바꾸게 됩니다. 이는 사용자가 이해하기 더 어려워지고, 러스트가 정말로 C와 C++을 대체할 수 있는 언어가 되고자 하는지에 대한 의심을 불러올 것입니다4.
얼마 전, Josh와 저는 러스트를 위한 설계 원칙 초안을 만들었습니다. 이를 다시 들여다보면, 이 질문에 대해 무엇을 말하는지 흥미롭습니다:
기본 박싱은 제 생각에 다음과 같이 평가됩니다:
(다른 원칙들은 두드러지게 영향을 받지 않는다고 봅니다.)
이러한 고려 끝에 Tyler와 저는 다른 설계로 방향을 잡았습니다. 앞서 언급한 “미래에서 온 사용자 가이드” 문서에서 보시듯, 그 설계는 지금까지의 실행 예제를 그대로는 받아들이지 않습니다. 대신, 지금까지 써 온 예제 코드를 컴파일하면 다음과 같은 오류가 납니다:
error[E0277]: 타입 `AI`는 어댑터 없이
`dyn AsyncIterator`로 변환될 수 없습니다
--> src/lib.rs:3:23
|
3 | use_dyn(&mut ai);
| ^^ `dyn AsyncIterator`로 변환하려면 어댑터가 필요합니다
|
= help: 각 async fn이 반환하는 future를 박싱하는
`Boxing` 어댑터 도입을 고려해 보세요
3 | use_dyn(&mut Boxing::new(ai));
++++++++++++ +
오류에서 제안하듯, 박싱 동작을 얻으려면 Boxing이라는 타입을 통해 옵트인해야 합니다5:
fn make_dyn<AI: AsyncIterator>(ai: AI) {
use_dyn(&mut Boxing::new(ai));
// ^^^^^^^^^^^
}
fn use_dyn(di: &mut dyn AsyncIterator) {
di.next().await;
}
이 설계에서는, 호출자가 next 메서드가 dyn*을 구성할 수 있는 타입을 반환한다는 것을 검증할 수 있을 때만 &mut dyn AsyncIterator를 만들 수 있습니다. 그게 아닌 경우가 대부분이므로, Boxing::new 어댑터를 사용해 Boxing<AI>를 만들 수 있습니다. 아직 전부 정교하게 다듬지는 않았지만6, 약간의 컴파일러 매직을 통해 Boxing<AI>를 dyn AsyncIterator로 강제 변환할 수 있습니다.
Boxing 타입의 세부는 더 손봐야 합니다7. 하지만 기본 아이디어는 같고, 기본 vtable 전략(실제로는 할당을 수행할 수도 있음)에 옵트인하도록 어떤 명시적 단계가 필요하다는 것입니다.
Boxing의 평가제 생각에 Boxing 어댑터를 추가하면 다음과 같이 평가됩니다…
dyn AsyncIterator를 만들 때마다 Boxing::new를 추가해야 하는 것은 썩 좋진 않지만, 다른 러스트의 잔소리들과 비슷한 수준입니다.이 설계는 이제 투명합니다. 생산성은 이전보다 낮아졌지만, 지원성으로 보완하려 했습니다. “러스트는 항상 쉽진 않지만, 늘 친절합니다.”
“기본은 박싱” 전략에서 마음에 걸리는 점 하나는 성능이 “그저 그렇다”는 것입니다. 저는 Iterator처럼 “예쁜 코드”를 쓰면 타이트 루프가 나오는 이야기를 좋아합니다. “예쁜” async 코드를 쓰면 순진하고 중간 정도의 효율성만 나오는 것이 못내 아쉽습니다.
그렇지만, 이는 미래에—그리고 하위 호환을 지키면서—고칠 수 있다고 생각합니다. 아이디어는 가상 호출의 ABI를 확장해 호출자가 피호출자를 위해 “스크래치 공간”을 선택적으로 제공할 수 있게 하는 것입니다. 예를 들어, 바이너리를 분석해 필요한 스택 공간에 대한 좋은 추정을 얻을 수 있습니다(데이터플로 분석을 하거나 단순히 AsyncIterator의 모든 구현을 훑어보기만 해도). 그러면 호출자가 future를 위한 스택 공간을 예약하고, 그 포인터를 피호출자에게 넘길 수 있습니다—피호출자는 스택 공간이 부족할 때 등에는 여전히 할당을 선택 할 수 있지만, 일반적인 경우에는 그 공간을 활용할 수 있습니다.
흥미롭게도, 이렇게 하면 러스트의 “투명성” 이야기에는 다시 압박이 가해질 수 있다고 봅니다. 러스트는 성능을 위해 최적화에 크게 기대지만, 일반적으로 인라이닝 같은 단순하고 지역적인 최적화에 국한되어 왔습니다. 특히 프로시저 간 데이터플로를 요구하진 않았죠(물론 도움이 되고, LLVM은 이를 합니다). 하지만 잠재적 피호출자들을 처리하기 위해 얼마나 많은 스택 공간을 예약할지 잘 추정하려면 그 규칙을 어기게 됩니다(또한 부록 A에서 설명하듯 간단한 이스케이프 분석도 필요합니다). 이 모든 것은 약간의 ‘성능 예측 불가능성’을 더합니다. 그래도, 이는 큰 문제가 아니라고 봅니다. 특히 폴백은 단지 Box::new를 사용하는 것이고, 앞서 말했듯 대부분의 사용자에게는 그걸로 충분합니다.
물론, Boxing을 쓰고 싶지 않을 수도 있습니다. 다른 종류의 어댑터도 구성할 수 있고, 유사한 방식으로 동작합니다. 예를 들어, 인라이닝 어댑터는 다음처럼 보일 수 있습니다:
fn make_dyn<AI: AsyncIterator>(ai: AI) {
use_dyn(&mut InlineAsyncIterator::new(ai));
// ^^^^^^^^^^^^^^^^^^^^^^^^
}
InlineAsyncIterator<AI> 타입은 future를 저장할 추가 공간을 더합니다. 그래서 next 메서드가 호출되면 자기 자신의 필드에 future를 기록하고 이를 호출자에게 반환합니다. 유사하게, 캐시된 박스 어댑터는 &mut CachedAsyncIterator::new(ai) 같은 모양일 수 있으며, 필드를 사용해 생성된 Box를 캐시합니다.
인라인/캐시 어댑터 이름에 트레이트 이름이 포함되어 있다는 점에 눈치채셨을 겁니다. 이는 Boxing처럼 컴파일러 매직에 의존하지 않고, 최종 사용자가 작성하도록 의도되었기 때문입니다. 아직 임의의 트레이트 정의에 대해 제네릭하게 동작하는 방법은 없습니다(제안서에서는 매크로를 사용해 적응시키려는 트레이트마다 어댑터 타입을 생성합니다). 이는 나중에 꼭 해결하고 싶은 부분입니다. 어댑터가 어떻게 동작하는지 더 읽어보세요.
모든 것을 하나로 모아 일관된 설계 제안으로 정리해 봅시다:
임의의 타입 AI에서 dyn AsyncIterator로의 강제 변환은 허용되지 않습니다. 대신 어댑터를 선택해야 합니다:
Boxing을 원하게 될 텐데, 이 방식은 성능 프로파일이 괜찮고 “그냥 동작”합니다.InlineAsyncIterator나 CachingAsyncIterator 같은 다른 전략을 구현하는 자신만의 어댑터를 작성할 수 있습니다.구현 관점에서:
dyn* Future를 반환합니다. 호출자는 가상 디스패치를 통해 poll을 호출하고, future를 폐기할 준비가 되면 (가상) drop 함수를 호출할 수 있습니다.Boxing<AI>에 대해 생성된 vtable은 AI::next() future를 저장할 박스를 할당해 이를 사용해 dyn* Future를 만듭니다.InlineAsyncIterator<AI>는 AI::next() future를 래퍼의 필드에 저장하고, 그 필드에 대한 생포인터(raw pointer)를 가져와 이 포인터로부터 dyn* Future를 만듭니다.더 나은 성능을 위한 잠재적 미래 확장8:
Boxing 어댑터는 그런 스택 공간이 제공되면, 가능할 때 이를 사용하여 박싱을 피합니다. 이를 위해서는 사전 스택 예약 크기를 추정하는 컴파일러 분석이 결합되어야 합니다.이로써 사실상 어떤 패턴이든 표현할 수 있습니다. 런타임이 적절한 어댑터(예: TokioSideStackAdapter::new(ai))를 제공한다면, 사이드 스택조차 표현 하는 것이 가능합니다. 다만 사이드 스택이 대중화된다면, 이를 노출하는 더 표준적인 수단을 고려하고 싶습니다.
이 제안의 주요 단점은 다음과 같습니다:
Boxing::new를 써야 하므로 생산성과 학습성이 떨어집니다. 그러나 이는 투명성에 큰 타격을 피하게 해줍니다. 이것이 올바른 선택일까요? 아직 완전히 확신하진 못하지만, 마음은 점점 예스로 기웁니다. 또한 이는 미래에 재검토할 수도 있습니다(예: 기본 어댑터를 추가하는 식으로).우리가 표현하지 못하는 패턴이 하나 있습니다: “호출자가 최대 크기만큼 할당”. 이 패턴은 힙 할당이 필요 없음을 보장 합니다. 우리는 크레이트 경계의 공용 함수 등을 고려해야 하므로, 힙 할당을 피하려 시도 하는 휴리스틱이 최선입니다. 보장을 제공하려면 인자 타입을 &mut dyn AsyncIterator(어떤 async 이터레이터든 수용)에서 더 좁은 것으로 바꿔야 합니다. 이렇게 하면 스택 프레임을 탈출하는 future도 지원됩니다(아래 부록 A 참조). 아마도 이러한 세부는 중요하지 않고, 인라인 future나 휴리스틱이면 충분하겠지만, 그렇지 않다면 stackfuture 같은 크레이트가 여전히 선택지입니다.
댓글은 이 internals 스레드에 남겨 주세요. 감사합니다!
지금까지의 논의에서는 async 호출 뒤에 곧바로 await이 따라오는 경우를 가정했습니다. 그런데 future가 await되지 않고 힙이나 다른 위치로 이동된다면 어떻게 될까요?
fn foo(x: &mut dyn AsyncIterator<Item = u32>) -> impl Future<Output = Option<u32>> + ‘_ {
x.next()
}
박싱을 사용하는 경우 이런 코드는 전혀 문제가 되지 않습니다. 하지만 future를 저장하기 위해 스택에 공간을 할당했다면, 이런 예제는 문제가 됩니다. 스크래치 공간이 선택 사항이고, 폴백으로 박싱이 가능하다면 문제가 없습니다. 이스케이프 분석을 통해 이런 예제에서는 스크래치 공간 사용을 피할 수 있습니다.
2020년 9월에 작성, 이런
Ada가 실제로 이런 방식을 쓴다는 점이 흥미로웠고, 가변 크기 타입 반환 같은 Ada의 기능이 이 모델 위에 구축되어 있음을 알게 되었습니다. SPARK나 임베디드 영역을 겨냥한 다른 Ada 서브셋이 이를 어떻게 처리하는지 확신하진 못합니다. 더 알아보고 싶습니다.↩︎
물론 사이드 스택이 없다면, 동적 디스패치나 재귀 같은 경우를 처리하기 위해 Box::new 같은 메커니즘을 쓰게 됩니다. 이는 필요한 추가 상태의 각 작은 조각마다 할당하는, 일종의 비관적으로 크기 잡힌 분할 스택(segmented stack)처럼 됩니다. 사이드 스택은 매력적인 중간지대일 수 있으나, embassy 같은 사례 때문에 유일한 옵션이 될 수는 없습니다.↩︎
역설적이게도, C++ 자체는 코루틴을 돕기 위해 암묵적인 힙 할당을 삽입합니다
더 좋은 이름 제안을 환영합니다.↩︎
커튼 뒤의 컴파일러 저자는 보지 마세요. 🪄 🌈 시선을 거두시길
예: 미래에서 온 사용자 가이드를 자세히 보면 &mut Boxing::new(ai)가 아니라 Boxing::new(&mut ai)라고 되어 있습니다. 이 부분은 저도 왔다 갔다 합니다.↩︎
명확히 하자면, Tyler와 제가 이 주제를 논의하긴 했지만, 그가 어떻게 생각하는지는 모릅니다. ‘제안의 일부’라기보다는 제가 관심 있는 확장에 가깝습니다.↩︎