C++26 이후 리플렉션 기반 프로그래밍에서 어노테이션과 구조적 바인딩 커스터마이제이션을 푸시 및 풀 방식으로 구현할 때의 장단점과 한계를 살펴봅니다.
거의 2년 전, 저는 Rust Attributes vs C++ Annotations라는 글을 썼습니다. 그 글에서는 표준 C++26에서 다음 코드를 어떻게 만들 수 있는지 설명했습니다:
struct [[=derive<Debug>]] Point {
int x;
int y;
};
다음의 표준 Rust 코드와 비슷한 동작과 효과를 내도록 말입니다:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
여기서
derive<Debug>라고 속성의 실제 표기를 정한 것은 본질적이지 않습니다. 그저 재미를 좀 보려 했을 뿐입니다. 저 템플릿에는 아무런 특별한 의미가 없습니다.
이 글에서는 그 목표를 달성하기 위한 여러 접근법과, 그것들이 어떻게… 완전히 잘 되지는 않는지를 살펴보겠습니다.
원래 접근법은 formatter의 부분 특수화를 제공하여, 어노테이션을 _풀_하고 내부적으로 모든 올바른 작업, 즉 데이터 멤버 순회 등을 수행하게 하는 것이었습니다:
template <class T> requires (has_annotation(^^T, derive<Debug>))
struct std::formatter<T> {
// ...
};
대다수 타입에서는 이 접근법이 훌륭하게 작동합니다. 문제는 모든 타입에서 보장되지는 않는다는 점입니다. 원래 글에서 지적했듯이, 우리가 부분 특수화를 하나 추가했을 뿐이므로 다른 부분 특수화가 일치하면(예를 들어 범위용 특수화) 특수화가 모호해집니다. 어노테이션을 추가했다는 것은 사용자의 의도상 이 동작을 원한다는 뜻이 아주 명확하다고 볼 수 있는데도, 오늘날 언어에는 이를 명시할 방법이 없습니다.
내일이라면 어떻게 명시할 방법을 고안할 수 있을지도 저는 잘 모르겠습니다.
상당히 실망스럽습니다. 대부분의 경우 작동하는 것도 꽤 좋지만, 저는 언제나 작동하는 해결책을 정말 원합니다.
올해 CppNow에서 기조연설을 했는데, 여기서 C++26 이후 리플렉션 관련 프로그래밍이 나아갈 좋은 방향이라고 생각하는 것을 이야기했습니다. 54:07에서 이 특정 문제를 푸는 아이디어를 제시했고, 다음 접근법을 제안했습니다(이번에도 불필요하게 재미를 노린 어노테이션 표기입니다):
template <class F>
struct derive {
F f;
consteval auto on_complete(std::meta::info r) const -> void {
f(r);
}
};
inline constexpr auto Debug = [](std::meta::info ty){
// 여기와 엄밀히 관련 없는 fmt_body를 구축하는
// 여러 작업을 수행하고, 마침내 ...
queue_injection(^^std, ^^{
template <>
struct formatter<$$ty$$> {
constexpr auto parse(auto& ctx) {
return ctx.begin();
}
auto format($$ty$$ const& object, auto& ctx) const {
$$fmt_body$$;
}
};
});
};
struct [[=derive(Debug)]] Point {
int x;
int y;
};
Debug 구현의 구체적인 세부 사항은 여기서 핵심이 아니지만, 컴파일러 탐색기에서 구현을 볼 수 있습니다. 아이디어는 어노테이션의 on_complete가 그것이 붙은 클래스가 완전해질 때 호출되고, 구현이 토큰 시퀀스를 사용해 구축한 적절한 특수화를 주입한다는 것입니다.
또는 CppCon에서 함께 다룬 접근법, 즉 원하는 디버그 포매팅을 구체적으로 수행하는 derive_formatter<T>를 사용하는 경우, 토큰 시퀀스를 최소한으로 사용하여 위 Debug를 다음처럼 구현할 수 있습니다:
inline constexpr auto Debug = [](std::meta::info ty){
queue_injection(^^std, ^^{
template <>
struct formatter<$$ty$$> : debug_formatter<$$ty$$> { };
});
};
그리고 연결한 구현에서 볼 수 있듯, 이것은 작동합니다. 클래스와 클래스 템플릿 모두에서 말입니다:
struct [[=derive(N::Debug)]] Config {
std::string name;
int amount;
};
template <class T>
struct [[=derive(N::Debug)]] Widget {
T thing;
};
이 모든 것을 기대한 방식으로 출력할 수 있습니다. 정말 훌륭합니다! 이제 부분 특수화가 아니라 명시적 특수화를 주입하므로, 이는 분명 모든 타입에 작동하는 해결책입니다.
그렇죠?
그렇죠?!
잠시 포매팅에서 벗어나 봅시다. 결국 다른 문제도 존재합니다. 구조적 바인딩에 대해 이야기하고 싶습니다. 오늘날 구조적 바인딩의 문제는 커스터마이제이션 지점을 작성해야 할 때 정말 번거롭다는 것입니다. 동일한 원천에서 비롯되는데도 서로 다른 세 가지 커스터마이제이션(tuple_size, tuple_element, get)을 구현해야 합니다.
그래서 하나의 원천에서 이 세 커스터마이제이션을 주입해 보는 실험을 하기로 했습니다. 대부분은 상당히 직관적입니다:
#include <meta>
#include <print>
consteval auto inject_structured_bindings(std::vector<std::meta::info> elems)
-> void
{
std::meta::info ty = parent_of(*elems.begin());
// tuple_size
queue_injection(^^std, ^^{
template <>
struct tuple_size<$$ty$$>
: integral_constant<size_t, $$elems.size()$$>
{ };
});
// 튜플 원소
for (size_t i = 0; i < elems.size(); ++i) {
queue_injection(^^std, ^^{
template <>
struct tuple_element<$$i$$, $$ty$$> {
using type = $$type_of(elems[i])$$;
};
});
}
// get은 지역적임
auto persisted = std::define_static_array(elems);
queue_injection(^^{
template <size_t I, class Self>
constexpr auto get(this Self&& self) -> decltype(auto) {
// 여기의 바깥 괄호는 실제로 필수적임
return (((Self&&)self).[: $$persisted.data()$$[I] :]);
}
});
}
template <class T>
class wide_result {
T hi;
T lo;
public:
constexpr wide_result(T hi, T lo) : hi(hi), lo(lo) { }
consteval {
inject_structured_bindings({^^hi, ^^lo});
}
};
auto main() -> int {
auto [hi, lo] = wide_result<uint64_t>(123, 456);
std::println("hi={}, lo={}", hi, lo); // hi=123, lo=456을 출력
}
이 함수에서 저는 tuple_size의 명시적 특수화 하나, tuple_elements의 명시적 특수화 N개(대신 부분 특수화 하나로 만들 수도 있을 법합니다), 그리고 지역 함수 템플릿 get을 주입하고 있습니다. 구현에서 어색한 부분은 persisted 변수 하나뿐입니다. get<I>는 I번째 바인딩을 생성해야 하지만 I는 get을 인스턴스화할 때까지 알 수 없는 반면, 우리는 지금 무언가를 주입해야 합니다. 코드 주입에서 흥미로운 점은 컴파일 도중 서로 다른 시점에 상수인 것들을 추론하려 한다는 것입니다. 이 특정 하위 문제에는 더 나은 접근법이 있을지도 모르며, 아직 생각해 봐야 합니다.
어쨌든 보시다시피 이것은 작동합니다.
적어도 static_assert를 추가하려 시도하기 전까지는 말입니다:
template <class T>
class wide_result {
T hi;
T lo;
public:
constexpr wide_result(T hi, T lo) : hi(hi), lo(lo) { }
consteval {
inject_structured_bindings({^^hi, ^^lo});
}
};
static_assert(std::tuple_size_v<wide_result<uint64_t>> == 2);
auto main() -> int {
// ...
}
앞선 프로그램이 작동했음을 생각하면 특히 놀랄 수도 있겠지만, 이 단언을 추가하면 프로그램이 망가집니다. 오류 메시지는 다음과 같습니다:
/cefs/48/486ef4e598174af0b0b3e1a2_clang-barry-clang-trunk-20260910/bin/../include/c++/v1/__tuple/tuple_size.h:63:40: error: implicit instantiation of undefined template 'std::tuple_size<wide_result<unsigned long>>'
63 | inline constexpr size_t tuple_size_v = tuple_size<_Tp>::value;
| ^
<source>:47:20: note: in instantiation of variable template specialization 'std::tuple_size_v<wide_result<unsigned long>>' requested here
47 | static_assert(std::tuple_size_v<wide_result<uint64_t>> == 2);
| ^
/cefs/48/486ef4e598174af0b0b3e1a2_clang-barry-clang-trunk-20260910/bin/../include/c++/v1/__tuple/tuple_size.h:27:8: note: template is declared here
27 | struct tuple_size;
| ^
tuple_size<wide_result<uint64_t>>가 정의되지 않은 템플릿이라니 무슨 뜻인가요? 제가 정의하지 않았나요? 정의된 것을 방금 보여 주지 않았나요?
Dan Katz가 표준에서 가장 좋아하는 부분은 [temp.point], 즉 “인스턴스화 지점”입니다. 템플릿이 실제로 작동하는 방식을 거의, 하지만 완전히는 아니게, 전혀 설명하지 않는 표준의 그 부분입니다.
간단히 말하면, 모든 템플릿에는 그 템플릿을 인스턴스화할 수 있는 지점(또는 지점들의 집합)이 있습니다. 위 wide_result<T> 구현에서 특정 특수화를 인스턴스화하면 inject_structured_bindings가 호출되고, 이어서 tuple_size, tuple_element, get에 필요한 커스터마이제이션 지점을 주입합니다. 하지만 이는 wide_result<T>를 인스턴스화할 때만 일어납니다. 식 tuple_size_v<X> == 2는 실제로 X의 인스턴스화를 요구하지 않으므로 그렇게 하지 않고, 따라서 consteval 블록은 평가되지 않으며, 커스터마이제이션 지점도 주입되지 않아 단언이 실패합니다.
사실 이보다도 나쁩니다.
wide_result<T>에tuple_size가 있는지 확인하는concept가 있고,wide_result<T>를 인스턴스화하기 전에 그concept를 확인한다면, 인스턴스화한 뒤에는 그concept의 답이 바뀝니다. 이는 우리 프로그램이 진단 불필요 ill-formed가 된다는 뜻입니다.
하지만 wide_result 특수화가 이미 인스턴스화되었다면 consteval 블록이 실행되고, 주입이 발생하며, 모든 것이 실제로는… 문제없습니다:
template <class T>
class wide_result {
T hi;
T lo;
public:
constexpr wide_result(T hi, T lo) : hi(hi), lo(lo) { }
consteval {
inject_structured_bindings({^^hi, ^^lo});
}
};
auto main() -> int {
auto [hi, lo] = wide_result<uint64_t>(123, 456);
std::println("hi={}, lo={}", hi, lo);
}
static_assert(std::tuple_size_v<wide_result<uint64_t>> == 2); // 문제없음
말할 것도 없이, 이것은 매우 취약합니다!
어노테이션이 템플릿에 적용되는 경우, 푸시 기반 포매팅 예제에서도 같은 문제가 발생합니다.
T가 인스턴스화되기 전에 타입T가formattable인지 확인하면 아직 특수화를 주입하지 않았으므로false결과를 관찰하게 됩니다. 포매팅 _자체_에서는 보통 단순 타입이 아니라 눈앞에 있는 객체에 대한 포매팅을 확인하므로(따라서 타입은 이미 인스턴스화되어 있으므로) 문제가 될 가능성이 작습니다. 하지만 가능성이 작다는 것은 결코 없다는 뜻은 아닙니다.
지금 문제는 우리가 다음을 주입하고 있다는 점입니다:
template <class T>
class wide_result { /* ... */ };
// wide_result<u64> 인스턴스화 시
namespace {
template <>
struct tuple_size<wide_result<u64>> {
// ...
};
}
한 접근법은 wide_result의 특수화에 일치하는 부분 특수화를 어떻게든 주입하는 것입니다. 다음과 같습니다:
template <class T>
class wide_result { /* ... */ };
// wide_result<T> 정의 직후
namespace {
template <class T>
requires (has_template_arguments(^^T)
and template_of(^^T) == ^^wide_result)
struct tuple_size<T> {
// ...
};
}
하지만 이것은 여전히 부분 특수화이므로, 풀 기반 어노테이션 모델에서 맞닥뜨리는 특수화 충돌과 정확히 같은 종류의 문제를 만날 수 있습니다. 이것은… 정확히 풀 기반 특수화이기 때문입니다.
유일한 진정한 접근법은 정확히 다음을 주입하는 것입니다:
template <class T>
class wide_result { /* ... */ };
// wide_result<T> 정의 직후
namespace {
template <class T>
struct tuple_size<wide_result<T>> {
// ...
};
}
맞습니다. 이것도 부분 특수화입니다. 하지만 가능한 한 가장 특수화된 것이므로 다른 일반적 충돌을 처리할 필요가 없습니다.
그러나 이는 두 가지 질문으로 이어집니다:
그 부분 특수화를 올바르게 주입하려면 이를 더 일찍 할 수 있어야 합니다. 클래스 템플릿 안에 consteval 블록을 둘 수는 없습니다. 아직 평가되지 않을 것이기 때문입니다. consteval 블록이 “클래스를 인스턴스화할 때”를 뜻하는 경우와 “템플릿 정의 지점에서”를 뜻하는 경우에 대한 규칙을 고안하려 하는 것은 지나치게 복잡해 보입니다. 그러므로 이를 수행하라고 알려 주는 신호가 본문 바깥에 필요합니다. 그리고 하나가 있습니다. 바로 어노테이션입니다.
template <class T>
class [[=inject_bindings]] wide_result {
// ...
};
이번에는 재미를 노린 이름이 없습니다. 하지만 이번에도 어노테이션이 붙은 엔터티가 완료될 때의 콜백은 있습니다. 이번에는 타입 완료가 아니라 템플릿 정의가 될 뿐입니다. 따라서 다음과 비슷할 것입니다:
struct inject_bindings_t {
consteval auto on_template_defined(std::meta::info tmpl) const -> void {
// ...
}
};
inline constexpr inject_bindings_t inject_bindings{};
이는 기본적으로 다음을 작성한 것처럼 동작합니다:
template <class T>
class wide_result {
// ...
};
consteval {
inject_bindings.on_template_defined(^^wide_result);
}
이제 wide_result의 모든 특수화에 일치하는 tuple_size 부분 특수화를 주입하기만 하면 됩니다. 그런데 우리가 가진 것은… 클래스 템플릿뿐입니다. 어떻게 해야 하는지 어떻게 알 수 있을까요?
지금까지 일반적인 C++ 접근법은 가변 클래스 템플릿에 단순히 일치시키는 것입니다. 다음과 같을 것입니다:
struct inject_bindings_t {
consteval auto on_template_defined(std::meta::info tmpl) const -> void {
queue_injection(^^std, ^^{
template <class... Ts>
struct tuple_size<$$tmpl$$<Ts...>> {
// ...
};
});
// tuple_element에도 유사하게 적용
}
};
이는 템플릿 매개변수를 어떤 개수(하나)든 받고 모두 타입인 wide_result에서는 분명 작동합니다. 하지만 일반적 해결책은 아닙니다. 상수 템플릿 매개변수나 템플릿 템플릿 매개변수가 있는 타입에서는 작동하지 않을 것입니다(이제는 콘셉트 템플릿 매개변수나 변수 템플릿 매개변수도 마찬가지입니다). 그리고 여기서 핵심은 일반적 해결책을 원한다는 것입니다. 일반적 해결책은 어떤 모습일까요?
이를 위한 것이 보편 템플릿 매개변수입니다. 이 기능에는 많은 동기 부여 사례가 있지만(논문을 보세요), 이 사례는 가능한 가장 단순한 사용법이라는 점에서 특히 성가롭습니다. 여기서는 템플릿 매개변수가 실제로 무엇인지 관심조차 없습니다. 전체 타입과 그것이 이 특정 패턴을 갖는다는 사실만 신경 쓰기 때문에 절대 들여다보려 하지 않을 것입니다. 이 논문을 사용하면 다음을 주입하게 됩니다:
struct inject_bindings_t {
consteval auto on_template_defined(std::meta::info tmpl) const -> void {
queue_injection(^^std, ^^{
template <universal template... Ts>
struct tuple_size<$$tmpl$$<Ts...>> {
// ...
};
});
// tuple_element에도 유사하게 적용
}
};
이는 모든 클래스 템플릿에 작동하는 일반적 해결책입니다. 일반적 해결책을 위한 다른 접근법은 특정 템플릿에 대해 정확히 그 템플릿 헤드를 주입하는 방법을 고안하는 것입니다. 즉:
// 첫 번째 불완전한 해결책: 타입 매개변수에만 작동
template <class... Ts>
struct tuple_size<wide_result<Ts...>> { ... };
// 두 번째 해결책: 모든 클래스 템플릿에 작동
template <universal template... Ts>
struct tuple_size<wide_result<Ts...>> { ... };
// 세 번째 해결책: 정확히 템플릿 헤드를 작성
template <class T>
struct tuple_size<wide_result<T>> { ... };
그러려면 여기 시그니처의 서로 다른 두 부분을 생성할 도우미가 필요합니다. 토큰 시퀀스 ^^{ class T }를 생성하는 방법과 토큰 시퀀스 ^^{ T }를 생성하는 방법이 필요합니다. 충돌하지 않도록 매개변수 이름 자체의 접두사를 제공할 수 있는 방법도 필요할 것입니다. 이름 자체는 중요하지 않기 때문입니다. 이 함수의 시그니처는 아마 다음과 비슷할 수 있습니다:
struct template_head_result {
std::meta::token_sequence head;
std::meta::token_sequence args;
};
consteval auto template_head_of(std::meta::info tmpl, std::string_view prefix)
-> template_head_result;
그러면 여기서 사용법은 다음과 같습니다:
struct inject_bindings_t {
consteval auto on_template_defined(std::meta::info tmpl) const -> void {
auto [head, args] = template_head_of(tmpl, "p");
queue_injection(^^std, ^^{
template <$$head$$>
struct tuple_size<$$tmpl$$<$$args$$>> {
// ...
};
});
// tuple_element에도 유사하게 적용
}
};
템플릿 헤드는 임의로 복잡할 수 있다는 점에 유의하세요. 제약된 선언을 포함할 수도 있고 이름을 재사용할 수도 있습니다. 예를 들어 std::integral_constant는 다음과 같습니다:
template <class T, T v>
struct integral_constant;
따라서 template_head_of(^^integral_constant, "p").head는 ^^{ class p0, p0 p1 }와 비슷한 것을 생성해야 하며, 절대로 ^^T{ class p0, T p1 }를 생성해서는 안 됩니다.
제가 프로그래밍 언어 이론이나 람다 대수에 대해 조금이라도 알았다면, α-변환 같은 것을 이야기했을 것입니다.
좋습니다. 올바른 특수화를 제대로 _푸시_하는 세 가지 선택지(타입만 사용하기, 보편 템플릿 매개변수 사용하기, 올바른 템플릿 헤드를 합성하는 전용 리플렉션 함수 사용하기)가 있습니다. 하지만 그 형태를 얻은 뒤에는 무엇을 할까요?
구조적 바인딩 주입의 초기 구현에서는 비정적 데이터 멤버를 나타내는 리플렉션의 벡터를 모든 주입을 수행하는 함수에 전달했습니다. 이 모든 것을 어노테이션으로 구동한다면 그렇게는 제대로 할 수 없습니다. 어노테이션은 클래스 바깥, 즉 비정적 데이터 멤버가 선언되기 전에 위치하기 때문입니다. 이는 작업을 나누어야 한다는 뜻입니다. 올바른 특수화는 _푸시_하되, 그 특수화들이 데이터를 다시 _풀_하도록 해야 합니다.
즉, 사용법은 다음과 비슷해질 것입니다:
template <class T>
class [[=inject_bindings]] wide_result { // <== 푸시-미
T hi;
T lo;
public:
constexpr wide_result(T hi, T lo) : hi(hi), lo(lo) { }
static constexpr info tuple_elements[] = {^^hi, ^^lo}; // <== 풀-미
};
어노테이션은 필요한 모든 조각을 주입하고, static constexpr 데이터 멤버는… 그 어노테이션의 매개변수입니다. 이 둘이 너무 멀리 떨어져 있다는 점은 조금 만족스럽지 않습니다. 하지만 여기서 tuple_elements는 nonstatic_data_members_of(^^C)를 기본값으로 삼을 수도 있으므로, 아마 그렇게 큰 문제는 아닐 것입니다.
이전에는 모호한 특수화 가능성 때문에 풀 기반 커스터마이제이션이 문제가 되었습니다. 하지만 올바른 특수화를 푸시한 뒤에는 더는 문제가 아니며, 데이터를 풀하는 것은 괜찮습니다.
구체적으로 다음을 주입할 수 있습니다(여전히 모든 타입 매개변수를 가정하는 특수화만 주입한다는 점에 유의하세요):
template <class T, template <class...> class Z>
concept specializes = has_template_arguments(remove_cvref(^^T))
and template_of(remove_cvref(^^T)) == ^^Z;
struct inject_bindings_t {
consteval auto on_template_defined(std::meta::info tmpl) const -> void {
queue_injection(^^std, ^^{
template <class... Ts>
struct tuple_size<$$tmpl$$<Ts...>>
: integral_constant<size_t, size($$tmpl$$<Ts...>::tuple_elements)>
{ };
template <size_t I, class... Ts>
struct tuple_element<I, $$tmpl$$<Ts...>> {
using type = [: type_of($$tmpl$$<Ts...>::tuple_elements[I]) :];
};
});
queue_injection(parent_of(tmpl), ^^{
template <size_t I, ::lib::specializes<$$tmpl$$> Self>
constexpr auto get(Self&& self) -> decltype(auto) {
return (((Self&&)self).[: self.tuple_elements[I] :]);
}
});
}
};
완전 수식한 ::lib::specializes는 이 어노테이션이 실제로 네임스페이스 lib에 있다고 가정하기 때문입니다. 그리고 get 내부에서 self.tuple_elements[I]를 어떻게 접합할 수 있는지 궁금하다면, constexpr 배열 크기 문제에 관한 제 글을 보세요.
이제 이 구현은 작동합니다. wide_result를 인스턴스화하기 전에 static_assert를 두어도 말입니다.
위 해결책이 있어도 저는 여전히 완전히 만족스럽지 않습니다. 먼저 이미 언급한 특수화의 형태 문제가 있습니다. 주어진 템플릿에 맞는 올바른 템플릿 헤드를 생성하는 메커니즘이나 보편 템플릿 매개변수가 실제로 필요합니다. 게다가 get을 네임스페이스 범위에 주입해야 한다는 점도 마음에 들지 않습니다. 이상적으로는 네임스페이스를 오염시키지 않도록 wide_result에 주입하고 싶습니다.
하지만 더 큰 문제가 있습니다. 언제나 더 큰 문제가 있기 때문입니다.
다음 형태의 클래스를 생각해 봅시다:
template <class T>
struct Outer {
struct Inner {
// ...
};
};
어떤 트레이트(formatter, tuple_size, 또는 다른 커스터마이제이션 지점이든)를 위한 특수화를 푸시하고 싶다면, 올바른 표기를 알려 주는 신탁이 있더라도 결과적으로 생성하게 될 표기는 다음과 같습니다:
template <class T>
struct TRAIT<Outer<T>::Inner> {
// ...
};
그런데… 이것은 작동하지 않습니다. 그 패턴은 비추론 문맥이므로 어떤 것과도 일치하지 않을 것입니다. 한편으로 일반적으로 그럴 만한 타당한 이유가 있습니다. Inner가 자체 타입이 아니라 실제로 using Inner = int;인 경우라면, 그로부터 T를 추론할 수 없음은 명백하기 때문입니다. 하지만 다른 한편으로 Inner가 별칭이 아니라면, T는 매우 명확하게 추론 가능합니다.
이것은 이 영역에서 이미 알려진 문제이며, 일부 라이브러리가 이런 문맥에서 중첩 타입을 피하려 하는 이유입니다. 언제나 코드를 다음처럼 재구성할 수 있습니다:
template <class T>
struct Inner {
// ...
};
template <class T>
struct Outer {
// ...
};
그저… 지역성만 고려했을 때 순전히 그렇게 해야 한다는 것이 성가십니다. 여러 이유로 Inner가 실제로 Outer의 중첩 클래스이기를 원할 수 있으므로, 언어 제한을 우회하려고 Outer 밖에 두어야 하는 것은 언제나 짜증 납니다. 어쩌면 이 규칙을 재고할 이유가 될 수도 있습니다. 그렇지 않으면 Outer<T>::Inner는 비추론 가능하므로, 그런 타입은 오직 풀 기반으로만 커스터마이즈할 수 있고 푸시 기반으로는 절대 할 수 없습니다.
저는 이 영역에서 계속 여러 가지를 시도하고 무엇이 작동하는지 살펴볼 것입니다. 하지만 이 글을 쓴 동기의 일부는 컴파일러 탐색기에 이 구현을 올려 두었으면서도 그 존재를 널리 알리는 일을 별로 하지 않았다는 사실을 깨달았기 때문이기도 합니다. 9월 메일링에 추가 예제 링크를 담은 업데이트된 토큰 시퀀스 주입 논문을 내놓고 싶습니다. 이 글에도 이미 몇 가지 예제가 있지만, 아마 제가 검토해 온 것 중 더 흥미로운 예제는 다음과 같습니다:
여러분이 무엇을 생각해 낼지 궁금합니다. 그냥 작동하는 무엇을 시도할지, 작동하지는 않지만 작동해야 하는 무엇을 시도할지, 또 다른 도구(혹은 다른 형태의 도구)가 필요한 무엇을 하고 싶을지 말입니다. 해 봅시다!