Date: Tue, 10 Feb 2026 16:31:54 +0000
Hi Roman,
Thanks for sharing this. Perhaps we can add it to the agenda in the next
meeting (tomorrow?).
I've left a few comments below...
On Sat, 7 Feb 2026 at 20:03, Root Tool via SG14 <sg14_at_[hidden]>
wrote:
> Hi SG14,
> I'm a 17-year-old hobbyist programmer with a background in Unreal Engine
> and embedded systems. I've implemented a proof-of-concept for a
> null-propagating member access operator '?->' in Clang and would like to
> share it for feedback.
>
> *Motivation*
> In game development (Unreal Engine for example), this pattern appears
> constantly:
>
>> UMySubsystem* UMySubsystem::Get(const UObject* WorldContextObject)
>> {
>> if(!WorldContextObject) return nullptr;
>
> What does it mean for this pointer to be null? If it is correct that it
can be null, then the function name isn't entirely accurate as it doesn't
always 'get' the desired object.
Perhaps we don't "want" the pointer to be null but sometimes, the caller is
careless and passes null anyway. If so, maybe that's a bug in the calling
code, in which case do we really want to be 'helping' this way? Instead one
might wish to change the contract of the function so that null isn't
allowed, maybe by:
* using a reference instead of a pointer,
* specifying a precondition (using a comment, or `pre`),
* asserting here instead, `check(WorldContextObject != nullptr)`, though
you'll like halt program execution even if you do nothing.
One way or another, we'd be saying "don't call this function if you don't
have the requisite object to hand".
>
>
> UWorld* World = WorldContextObject->GetWorld();
>
> if(!World) return nullptr;
>
> Same question: is it ever OK for this to be null? And semantically, what
does that mean? I'm guessing this only happens if the engine is not done
starting up. It would seem like a precondition that you cannot get
subsystems until the system is initialised.
>
>
> UGameInstance* GI = World->GetGameInstance();
>>
> if(!GI) return nullptr;
>
> Similar thing here.
>
>
> return GI->GetSubsystem<UMySubsystem>();
>
> }
>
> Or in C++17 with "if with initializer":
>
>> UMySubsystem* UMySubsystem::Get(const UObject* WorldContextObject)
>> {
>> if(WorldContextObject)
>> if(UWorld* World = WorldContextObject->GetWorld())
>> if(UGameInstance* GI = World->GetGameInstance())
>> return GI->GetSubsystem<UMySubsystem>();
>
> return nullptr;
>> }
>
> And with '?->' this becomes:
>
>> UMySubsystem* UMySubsystem::Get(const UObject* WorldContextObject)
>> {
>> return
>> WorldContextObject?->GetWorld()?->GetGameInstance()?->GetSubsystem<UMySubsystem>();
>
> }
>
>
> *What I implemented*
> Parser change (ParseExpr.cpp):
> - Handle '?->' token in postfix expression suffix
> - Distinguish field access (obj?->field) vs method call (obj?->method())
> - Parse method arguments immediately to avoid intermediate state
> Sema change (SemaExprMember.cpp):
> - Lower '?->' to ConditionalOperator with OpaqueValueExpr for CSE
> - Condition: explicit != nullptr comparison (no implicit bool conversion)
> - Result type must be pointer or void (compile-time check)
> - Chainable: each '?->' nests properly
>
> *Example*
> C++:
>
>> struct AActor { void Destroy() {} };
>>
>> struct UWorld
>> {
>> AActor Actor = AActor();
>> AActor* GetActor() { return &Actor; }
>
> };
>>
>> UWorld MyWorld = UWorld();
>> UWorld* GetWorld() { return &MyWorld; }
>>
>> int main()
>> {
>> GetWorld()?->GetActor()?->Destroy();
>> return 0;
>> }
>
> AST (simplified):
>
>> ConditionalOperator 'void'
>> |-BinaryOperator 'bool' '!='
>> | |-OpaqueValueExpr 'AActor*'
>> | | `-CallExpr 'AActor*' GetActor
>> | `-CXXNullPtrLiteralExpr
>> |-CXXMemberCallExpr 'void' Destroy
>> | `-MemberExpr 'AActor*'
>> `-ImplicitCastExpr 'void' <ToVoid>
>> `-IntegerLiteral 'int' 0
>
>
> *Key properties visible in AST*
> - GetWorld() called once, stored in OpaqueValueExpr
> - Explicit != nullptr comparison (BinaryOperator, not implicit cast)
> - GetActor() result reused in condition and call
> - Proper nesting for chaining
>
> *Limitations (honest)*
> - Frontend only: I don't know LLVM IR/backend, so this is purely AST
> transformation
> - No new AST node: Reuses ConditionalOperator instead of proper
> NullPropagatingExpr
> - No optimizations: I rely on existing CSE and hope LLVM optimizes the
> nested ternary
> - Not production-ready: Missing error recovery, some edge cases with
> templates
>
> *Design Decisions - p**ointer and void types only*
> I intentionally restricted ?-> to expressions where the result is a
> pointer or void. These are the only two types where we can safely represent
> "something exists" versus "nothing" (nullptr).
> For pointers - the semantics are clear: either we have a valid pointer to
> the member, or we return nullptr.
> For void methods - we either execute the call or do nothing (void).
> Other types don't have this natural "null" representation. What should
> "obj?->field" return if the field is int? Zero? Negative one? A magic
> constant? What if the field is an object type like AActor? Return a
> default-constructed instance? That changes semantics default constructor
> might have side effects. The answer depends entirely on the domain, so I
> chose to forbid it rather than guess wrong.
>
>> // Valid
>> UWorld* Wold = Object?->GetWorld(); // pointer - OK
>> World?->Tick(); // void - OK
>
>
>> // Invalid - compile error
>> int x = obj?->GetWorld()?->GetID(); // ERROR: int is not pointer or void
>
>
> *Questions for SG14*
> - Is this direction worth pursuing formally, or does it need a different
> approach?
>
Have you considered a library-side solution involving a metaclass-based
smart pointer?
> - Is the "pointer or void result only" restriction correct for game dev
> use cases?
>
I'm not sure we can answer that, though individual game devs here can share
their opinions. I doubt its value would be limited to game development.
> - Who could help with the LLVM IR side if I continue?
> - And what is the path to standardization? Should this go through EWG, or
> is there prior art (P-paper number) I should be aware of? What would make
> this "C++29-ready"?
>
> I'm not affiliated with any company, just a student who writes C++ daily.
> Any feedback appreciated.
>
- Have you looked for existing proposals related to this? It seems like
something that might have been floated previously.
- Does this work in C?
- What do other languages do?
- How does this compare to monadic extensions to std::optional and other
similar patterns for handling disappointing pointers?
Cheers,
John
>
> Sincerely
> Roman Tikhostup
> GitHub:
> https://github.com/RootTool0/null-propagating-member-access-operator
> Unreal Engine / Embedded / C++ enthusiast
> _______________________________________________
> SG14 mailing list
> SG14_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/sg14
>
Thanks for sharing this. Perhaps we can add it to the agenda in the next
meeting (tomorrow?).
I've left a few comments below...
On Sat, 7 Feb 2026 at 20:03, Root Tool via SG14 <sg14_at_[hidden]>
wrote:
> Hi SG14,
> I'm a 17-year-old hobbyist programmer with a background in Unreal Engine
> and embedded systems. I've implemented a proof-of-concept for a
> null-propagating member access operator '?->' in Clang and would like to
> share it for feedback.
>
> *Motivation*
> In game development (Unreal Engine for example), this pattern appears
> constantly:
>
>> UMySubsystem* UMySubsystem::Get(const UObject* WorldContextObject)
>> {
>> if(!WorldContextObject) return nullptr;
>
> What does it mean for this pointer to be null? If it is correct that it
can be null, then the function name isn't entirely accurate as it doesn't
always 'get' the desired object.
Perhaps we don't "want" the pointer to be null but sometimes, the caller is
careless and passes null anyway. If so, maybe that's a bug in the calling
code, in which case do we really want to be 'helping' this way? Instead one
might wish to change the contract of the function so that null isn't
allowed, maybe by:
* using a reference instead of a pointer,
* specifying a precondition (using a comment, or `pre`),
* asserting here instead, `check(WorldContextObject != nullptr)`, though
you'll like halt program execution even if you do nothing.
One way or another, we'd be saying "don't call this function if you don't
have the requisite object to hand".
>
>
> UWorld* World = WorldContextObject->GetWorld();
>
> if(!World) return nullptr;
>
> Same question: is it ever OK for this to be null? And semantically, what
does that mean? I'm guessing this only happens if the engine is not done
starting up. It would seem like a precondition that you cannot get
subsystems until the system is initialised.
>
>
> UGameInstance* GI = World->GetGameInstance();
>>
> if(!GI) return nullptr;
>
> Similar thing here.
>
>
> return GI->GetSubsystem<UMySubsystem>();
>
> }
>
> Or in C++17 with "if with initializer":
>
>> UMySubsystem* UMySubsystem::Get(const UObject* WorldContextObject)
>> {
>> if(WorldContextObject)
>> if(UWorld* World = WorldContextObject->GetWorld())
>> if(UGameInstance* GI = World->GetGameInstance())
>> return GI->GetSubsystem<UMySubsystem>();
>
> return nullptr;
>> }
>
> And with '?->' this becomes:
>
>> UMySubsystem* UMySubsystem::Get(const UObject* WorldContextObject)
>> {
>> return
>> WorldContextObject?->GetWorld()?->GetGameInstance()?->GetSubsystem<UMySubsystem>();
>
> }
>
>
> *What I implemented*
> Parser change (ParseExpr.cpp):
> - Handle '?->' token in postfix expression suffix
> - Distinguish field access (obj?->field) vs method call (obj?->method())
> - Parse method arguments immediately to avoid intermediate state
> Sema change (SemaExprMember.cpp):
> - Lower '?->' to ConditionalOperator with OpaqueValueExpr for CSE
> - Condition: explicit != nullptr comparison (no implicit bool conversion)
> - Result type must be pointer or void (compile-time check)
> - Chainable: each '?->' nests properly
>
> *Example*
> C++:
>
>> struct AActor { void Destroy() {} };
>>
>> struct UWorld
>> {
>> AActor Actor = AActor();
>> AActor* GetActor() { return &Actor; }
>
> };
>>
>> UWorld MyWorld = UWorld();
>> UWorld* GetWorld() { return &MyWorld; }
>>
>> int main()
>> {
>> GetWorld()?->GetActor()?->Destroy();
>> return 0;
>> }
>
> AST (simplified):
>
>> ConditionalOperator 'void'
>> |-BinaryOperator 'bool' '!='
>> | |-OpaqueValueExpr 'AActor*'
>> | | `-CallExpr 'AActor*' GetActor
>> | `-CXXNullPtrLiteralExpr
>> |-CXXMemberCallExpr 'void' Destroy
>> | `-MemberExpr 'AActor*'
>> `-ImplicitCastExpr 'void' <ToVoid>
>> `-IntegerLiteral 'int' 0
>
>
> *Key properties visible in AST*
> - GetWorld() called once, stored in OpaqueValueExpr
> - Explicit != nullptr comparison (BinaryOperator, not implicit cast)
> - GetActor() result reused in condition and call
> - Proper nesting for chaining
>
> *Limitations (honest)*
> - Frontend only: I don't know LLVM IR/backend, so this is purely AST
> transformation
> - No new AST node: Reuses ConditionalOperator instead of proper
> NullPropagatingExpr
> - No optimizations: I rely on existing CSE and hope LLVM optimizes the
> nested ternary
> - Not production-ready: Missing error recovery, some edge cases with
> templates
>
> *Design Decisions - p**ointer and void types only*
> I intentionally restricted ?-> to expressions where the result is a
> pointer or void. These are the only two types where we can safely represent
> "something exists" versus "nothing" (nullptr).
> For pointers - the semantics are clear: either we have a valid pointer to
> the member, or we return nullptr.
> For void methods - we either execute the call or do nothing (void).
> Other types don't have this natural "null" representation. What should
> "obj?->field" return if the field is int? Zero? Negative one? A magic
> constant? What if the field is an object type like AActor? Return a
> default-constructed instance? That changes semantics default constructor
> might have side effects. The answer depends entirely on the domain, so I
> chose to forbid it rather than guess wrong.
>
>> // Valid
>> UWorld* Wold = Object?->GetWorld(); // pointer - OK
>> World?->Tick(); // void - OK
>
>
>> // Invalid - compile error
>> int x = obj?->GetWorld()?->GetID(); // ERROR: int is not pointer or void
>
>
> *Questions for SG14*
> - Is this direction worth pursuing formally, or does it need a different
> approach?
>
Have you considered a library-side solution involving a metaclass-based
smart pointer?
> - Is the "pointer or void result only" restriction correct for game dev
> use cases?
>
I'm not sure we can answer that, though individual game devs here can share
their opinions. I doubt its value would be limited to game development.
> - Who could help with the LLVM IR side if I continue?
> - And what is the path to standardization? Should this go through EWG, or
> is there prior art (P-paper number) I should be aware of? What would make
> this "C++29-ready"?
>
> I'm not affiliated with any company, just a student who writes C++ daily.
> Any feedback appreciated.
>
- Have you looked for existing proposals related to this? It seems like
something that might have been floated previously.
- Does this work in C?
- What do other languages do?
- How does this compare to monadic extensions to std::optional and other
similar patterns for handling disappointing pointers?
Cheers,
John
>
> Sincerely
> Roman Tikhostup
> GitHub:
> https://github.com/RootTool0/null-propagating-member-access-operator
> Unreal Engine / Embedded / C++ enthusiast
> _______________________________________________
> SG14 mailing list
> SG14_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/sg14
>
Received on 2026-02-10 16:32:09
