Date: Wed, 11 Feb 2026 12:48:16 +0300
Hi John,
Thank you for your comments.
Unfortunately, I won't be able to attend tomorrow's meeting, but I am
available to answer any questions in writing. I hope the group can discuss
my proposal even in my absence.
Key points I would like to emphasize:
1) Feedback from Unreal Engine developers would be extremely valuable - I
am particularly interested to know if the proposed limitation (pointers and
'void' only) addresses real problems in their codebases.
2) For hands-on exploration, I will prepare a folder named "ForSG14" in my
repository "null-propagating-member-access-operator" containing:
- A Clang build for Win x64
- Test files
Answers to your questions:
0) "When is nullptr not an error?"
In Unreal Engine (and game development in general), 'nullptr' is often a
valid state:
- Failed cast (e.g., 'Cast<ADoor>(Actor)' returns 'nullptr' if the object
is not a door)
- Uninitialized object
- Missing resource
Example of a typical check:
"""
AActor* Actor = ...;
ADoor* Door = Cast<ADoor>(Actor);
if(Door) Door->Open();
"""
With the '?->' operator, this becomes:
"""
Cast<ADoor>(Actor)?->Open();
"""
If the cast fails, the call is simply skipped. This aligns with the LBYL
(Look Before You Leap) paradigm and prevents accidental crashes in long
call chains.
1) "Have you looked for existing proposals related to this? It seems like
something that might have been floated previously."
Yes, I found two mentions of similar proposals:
- P2561R2 ("Error Continuations") touches on similar ideas for handling
null states:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2561r2.html#error-continuations
- The topic of a '?->' operator has been raised repeatedly in the community
(e.g., in Reddit discussions):
https://www.reddit.com/r/cpp/comments/kxflj9/possibility_of_adding_operators_to_handle_nullptr/
The main challenge encountered in these discussions has been determining
the return type for non-pointer types. My proposal is intentionally limited
to pointers and void to address the most common cases without unnecessary
complexity.
2) "Does this work in C?"
Yes, the semantics of the operator naturally extend to the C language, as
checking pointers for 'NULL' is a common problem for both C and C++. The
syntax '?->' logically extends the existing concept of the conditional
operator '?:'
The attached materials will also include tests for the C language.
3) "What do other languages do?"
Safe member access operators are a common pattern in modern languages:
- C#: obj?.Member (since version 6.0)
- Kotlin: obj?.member
- Swift: obj?.member
- TypeScript/JavaScript: obj?.member (ES2020)
In all these languages, the operator returns:
- null/nil if any element in the chain is null
- Otherwise, the result of the last operation
My '?->' operator adapts this proven idea for C++:
- Uses the familiar '->' syntax for pointers
- Preserves the "check for nullptr and stop" semantics
- Key difference: limited to pointers and void only
Why don't I add support for 'std::optional<T>' like C# does with
'Nullable<T>'?
1) Overhead - 'std::optional' introduces overhead unacceptable in game
development and low-latency contexts
2) Backward compatibility - the operator works with existing APIs without
requiring wrapping in optional
3) Simplicity - for 80% of cases in game engines, pointer checks are
sufficient
This is a deliberate design choice: to provide maximum benefit with minimal
complexity.
4) "How does this compare to monadic extensions to std::optional and other
similar patterns for handling disappointing pointers?"
If you are referring to monadic interfaces like 'std::optional::and_then'
from C++23 - yes, I have considered this approach.
Similarity: Both approaches allow chaining operations with a check for an
"empty" state.
Key differences:
1) Scope of application:
- 'std::optional' - for value semantics (return values, parameters)
- '?->' - for pointer semantics (existing APIs, legacy code, game engines)
2) Overhead:
- 'std::optional' adds storage overhead (at least 1 byte + alignment for
the 'has_value' flag)
- '?->' leverages the existing nature and null-checking of raw pointers
(zero-overhead)
3) Ease of use:
- 'optional.and_then(f).and_then(g)' requires lambdas, which:
- Complicates syntax (capture lists, dangling references)
- Increases binary size even with inlining
- 'ptr?->f()?->g()' - an explicit, readable syntax meaning "call if
possible"
4) Performance in hot paths:
- In games, thousands of objects are updated every frame (16.6 ms)
- Even minor 'std::optional' overhead accumulates
- '?->' compiles to a simple 'if (ptr != nullptr)' check
Conclusion: '?->' does not replace 'std::optional', but complements it for
a specific niche:
- Pointers (especially in legacy and game engine code)
- Situations where performance is critical
- Cases where changing an API to use 'std::optional' is not feasible
These are two different tools for different tasks.
Sincerely
Roman Tikhostup
GitHub: https://github.com/RootTool0/null-propagating-member-access-operator
вт, 10 февр. 2026 г., 19:32 John McFarlane <john_at_[hidden]>:
> 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
>>
>
Thank you for your comments.
Unfortunately, I won't be able to attend tomorrow's meeting, but I am
available to answer any questions in writing. I hope the group can discuss
my proposal even in my absence.
Key points I would like to emphasize:
1) Feedback from Unreal Engine developers would be extremely valuable - I
am particularly interested to know if the proposed limitation (pointers and
'void' only) addresses real problems in their codebases.
2) For hands-on exploration, I will prepare a folder named "ForSG14" in my
repository "null-propagating-member-access-operator" containing:
- A Clang build for Win x64
- Test files
Answers to your questions:
0) "When is nullptr not an error?"
In Unreal Engine (and game development in general), 'nullptr' is often a
valid state:
- Failed cast (e.g., 'Cast<ADoor>(Actor)' returns 'nullptr' if the object
is not a door)
- Uninitialized object
- Missing resource
Example of a typical check:
"""
AActor* Actor = ...;
ADoor* Door = Cast<ADoor>(Actor);
if(Door) Door->Open();
"""
With the '?->' operator, this becomes:
"""
Cast<ADoor>(Actor)?->Open();
"""
If the cast fails, the call is simply skipped. This aligns with the LBYL
(Look Before You Leap) paradigm and prevents accidental crashes in long
call chains.
1) "Have you looked for existing proposals related to this? It seems like
something that might have been floated previously."
Yes, I found two mentions of similar proposals:
- P2561R2 ("Error Continuations") touches on similar ideas for handling
null states:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2561r2.html#error-continuations
- The topic of a '?->' operator has been raised repeatedly in the community
(e.g., in Reddit discussions):
https://www.reddit.com/r/cpp/comments/kxflj9/possibility_of_adding_operators_to_handle_nullptr/
The main challenge encountered in these discussions has been determining
the return type for non-pointer types. My proposal is intentionally limited
to pointers and void to address the most common cases without unnecessary
complexity.
2) "Does this work in C?"
Yes, the semantics of the operator naturally extend to the C language, as
checking pointers for 'NULL' is a common problem for both C and C++. The
syntax '?->' logically extends the existing concept of the conditional
operator '?:'
The attached materials will also include tests for the C language.
3) "What do other languages do?"
Safe member access operators are a common pattern in modern languages:
- C#: obj?.Member (since version 6.0)
- Kotlin: obj?.member
- Swift: obj?.member
- TypeScript/JavaScript: obj?.member (ES2020)
In all these languages, the operator returns:
- null/nil if any element in the chain is null
- Otherwise, the result of the last operation
My '?->' operator adapts this proven idea for C++:
- Uses the familiar '->' syntax for pointers
- Preserves the "check for nullptr and stop" semantics
- Key difference: limited to pointers and void only
Why don't I add support for 'std::optional<T>' like C# does with
'Nullable<T>'?
1) Overhead - 'std::optional' introduces overhead unacceptable in game
development and low-latency contexts
2) Backward compatibility - the operator works with existing APIs without
requiring wrapping in optional
3) Simplicity - for 80% of cases in game engines, pointer checks are
sufficient
This is a deliberate design choice: to provide maximum benefit with minimal
complexity.
4) "How does this compare to monadic extensions to std::optional and other
similar patterns for handling disappointing pointers?"
If you are referring to monadic interfaces like 'std::optional::and_then'
from C++23 - yes, I have considered this approach.
Similarity: Both approaches allow chaining operations with a check for an
"empty" state.
Key differences:
1) Scope of application:
- 'std::optional' - for value semantics (return values, parameters)
- '?->' - for pointer semantics (existing APIs, legacy code, game engines)
2) Overhead:
- 'std::optional' adds storage overhead (at least 1 byte + alignment for
the 'has_value' flag)
- '?->' leverages the existing nature and null-checking of raw pointers
(zero-overhead)
3) Ease of use:
- 'optional.and_then(f).and_then(g)' requires lambdas, which:
- Complicates syntax (capture lists, dangling references)
- Increases binary size even with inlining
- 'ptr?->f()?->g()' - an explicit, readable syntax meaning "call if
possible"
4) Performance in hot paths:
- In games, thousands of objects are updated every frame (16.6 ms)
- Even minor 'std::optional' overhead accumulates
- '?->' compiles to a simple 'if (ptr != nullptr)' check
Conclusion: '?->' does not replace 'std::optional', but complements it for
a specific niche:
- Pointers (especially in legacy and game engine code)
- Situations where performance is critical
- Cases where changing an API to use 'std::optional' is not feasible
These are two different tools for different tasks.
Sincerely
Roman Tikhostup
GitHub: https://github.com/RootTool0/null-propagating-member-access-operator
вт, 10 февр. 2026 г., 19:32 John McFarlane <john_at_[hidden]>:
> 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
>>
>
Received on 2026-02-11 09:48:34
