Date: Wed, 12 Nov 2025 17:10:34 +0000
> Can someone speak to the intent in C23 ?
Interpretation 1 (the tokens are simply discarded by the preprocessor) was the original intent of this change for C23.
However, the intent was too strict. So the language that required interpretation 1 was removed in order to allow diagnostics to be practical.
Speaking in terms of "intent", I think something like
#define va_start(ap, ...) __builtin_va_start(ap) EAT_ONE(__VA_ARGS__)
#define EAT_ONE(X) /* allows empty */
...would be totally reasonable. Something like
#define va_start(ap, ...) ((void)sizeof (int){ __VA_ARGS__ }, __builtin_va_start(ap))
...that tries to get creative about forcing a one-element warning and then deactivates evaluation at the expression level, would also be in the spirit of the change.
The idea was that this definition:
#define va_start(ap, ...) __builtin_va_start(ap, __VA_ARGS__)
...should explicitly not be allowed. However, the Standard lacks the wording to say what powers a `__builtin` might or might not have. If the compiler can make this valid syntax when the second argument is missing, then ... it's doing something out of scope of the wording anyway. So although the original intent was to forbid this, it can't actually be expressed.
In my opinion (as a tool vendor), the `EAT_ONE` approach should be adequate for generating warnings, because of the context that in past versions of the Standard the trailing expression had to be a plain identifier. In practice, tooling will usually have enough contextual information to be able to check if such an identifier "would have been" in scope even if it's not ultimately expanded.
Additionally, the warning doesn't need to be especially smart. The other side of the intent was to encourage users not to put an identifier here at all, so a warning that simply checked if anything was passed after `ap` would do 90% of the important work too. In that case a different enforcement with a different underlying macro expansion might be suitable for pre-C23 modes.
Thanks,
Alex
[Perforce Software]
Alex Celeste | Elder Witch of the C Compiler, Perforce QAC
Perforce Software<https://www.perforce.com>
P +1 612.517.2100
Visit Us On: LinkedIn<https://www.linkedin.com/company/perforce> | YouTube<https://www.youtube.com/user/perforcesoftware>
________________________________
From: Liaison <liaison-bounces_at_[hidden]> on behalf of Joshua Berne via Liaison <liaison_at_[hidden]>
Sent: 12 November 2025 16:51
To: liaison_at_[hidden] <liaison_at_lists.isocpp.org>
Cc: Joshua Berne <berne_at_[hidden]>
Subject: Re: [isocpp-wg14/wg21-liaison] Extra arguments to va_start
On Wed, Nov 12, 2025 at 11:39 AM Jens Maurer via Liaison <liaison_at_[hidden]<mailto:liaison_at_[hidden]>> wrote:
On 11/12/25 16:51, Aaron Ballman via Liaison wrote:
> This is why I'm asking SG22 to step in; this seems like a needless incompatibility between C and C++.
>
> void func(int i, ...) {
> va_list list;
>
> va_start(list, blah); // #1
> va_start(list, __COUNTER__); // #2
> }
>
> #1 is required to be diagnosed in C17 and C++23, is required to be accepted in C++26. C23 is unclear IMO. The standard says the argument is not evaluated. So if the implementation expands to `sizeof(blah)` for some reason, that's allowed by the standard wording for va_start because `blah` is not evaluated?
Obviously. In sizeof(blah), "blah" is not evaluated,
but name lookup must succeed.
> I think the intent for C23 is clear in that this code is expected to be semantically well-formed regardless of what it expands to,
What is the context in which "semantically well-formed" is determined?
> and there's a recommendation to diagnose if any additional arguments are passed.
There are three levels of ignored:
(1) preprocessing tokens are ignored (__COUNTER__ is not incremented):
#define va_start(list, ...) __builtin_va_start(list)
(2) tokens are ignored (__COUNTER__ is incremented, no phase 7 name lookup):
#define va_start(list, ...) __builtin_va_start(list, __VA_ARGS__ ???)
(3) tokens are not evaluated (__COUNTER__ is incremented, phase 7 name lookup must succeed):
#define va_start(list, ...) ( sizeof((__VA_ARGS__, 0)) , __builtin_va_start(list) )
(4) tokens are evaluated as an expression:
#define va_start(list, ...) ( (__VA_ARGS__, 0), __builtin_va_start(list) )
It's easy to to (1) and probably easy to do (3) (even though the "sizeof"
might not be exactly the right machinery), but I don't know how to do (2)
with current technology, absent a new macro-like built-in.
(2) seems doable by stringizing the parameters and then discarding the resulting string.
Can someone speak to the intent in C23 ?
I thought (1) was intended to be a valid implementation in C23,
with QoI diagnostics for lookup errors on the second argument.
(Yes, __COUNTER__ would not be incremented.)
If (1) is not a valid implementation, we need to be a lot more
precise what we expect in terms of macro expansion and phase 7 name lookup.
> #2 is required to be diagnosed in C17 and C++23, is required to be accepted in C23 and C++26, where __COUNTER__ is expected to increment in C and expected to not increment in C++.
>
> In both cases, I think this boils down to whether the committees expect something like:
>
> #define va_start(list, ...) __builtin_va_start(list, __VA_ARGS__)
>
> where the optional arguments passed to the macro are expanded and must be valid for a call to a builtin function. I think this is how WG14 expected things to work and so #1 would be rejected due to use of an undeclared identifier, similar to what would happen with `1 || blah` in an expression (`blah` is not evaluated, but it still has to be semantically valid). And #2 would increment __COUNTER__ because the macro is expanded for the call to the builtin.
>
> Or whether the committees expect something more like:
>
> #define va_start(list, ...) __builtin_va_start(list)
>
> where the optional arguments passed to the macro are not expanded and therefore don't have to be valid beyond what the preprocessor requires. I think this is how WG21 expected things to work, so #1 would be accepted and #2 would not increment __COUNTER__.
I don't know if WG21 "expects" anything; https://cplusplus.github.io/LWG/issue4388
is phrased very much like an alignment with C23 that was overlooked when
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3348r4.pdf
rebased the C library parts of C++ onto C23.
Jens
_______________________________________________
Liaison mailing list
Liaison_at_[hidden]<mailto:Liaison_at_[hidden]socpp.org>
Subscription: https://lists.isocpp.org/mailman/listinfo.cgi/liaison
Link to this post: http://lists.isocpp.org/liaison/2025/11/1598.php
CAUTION: This email originated from outside of the organization. Do not click on links or open attachments unless you recognize the sender and know the content is safe.
This e-mail may contain information that is privileged or confidential. If you are not the intended recipient, please delete the e-mail and any attachments and notify us immediately.
Interpretation 1 (the tokens are simply discarded by the preprocessor) was the original intent of this change for C23.
However, the intent was too strict. So the language that required interpretation 1 was removed in order to allow diagnostics to be practical.
Speaking in terms of "intent", I think something like
#define va_start(ap, ...) __builtin_va_start(ap) EAT_ONE(__VA_ARGS__)
#define EAT_ONE(X) /* allows empty */
...would be totally reasonable. Something like
#define va_start(ap, ...) ((void)sizeof (int){ __VA_ARGS__ }, __builtin_va_start(ap))
...that tries to get creative about forcing a one-element warning and then deactivates evaluation at the expression level, would also be in the spirit of the change.
The idea was that this definition:
#define va_start(ap, ...) __builtin_va_start(ap, __VA_ARGS__)
...should explicitly not be allowed. However, the Standard lacks the wording to say what powers a `__builtin` might or might not have. If the compiler can make this valid syntax when the second argument is missing, then ... it's doing something out of scope of the wording anyway. So although the original intent was to forbid this, it can't actually be expressed.
In my opinion (as a tool vendor), the `EAT_ONE` approach should be adequate for generating warnings, because of the context that in past versions of the Standard the trailing expression had to be a plain identifier. In practice, tooling will usually have enough contextual information to be able to check if such an identifier "would have been" in scope even if it's not ultimately expanded.
Additionally, the warning doesn't need to be especially smart. The other side of the intent was to encourage users not to put an identifier here at all, so a warning that simply checked if anything was passed after `ap` would do 90% of the important work too. In that case a different enforcement with a different underlying macro expansion might be suitable for pre-C23 modes.
Thanks,
Alex
[Perforce Software]
Alex Celeste | Elder Witch of the C Compiler, Perforce QAC
Perforce Software<https://www.perforce.com>
P +1 612.517.2100
Visit Us On: LinkedIn<https://www.linkedin.com/company/perforce> | YouTube<https://www.youtube.com/user/perforcesoftware>
________________________________
From: Liaison <liaison-bounces_at_[hidden]> on behalf of Joshua Berne via Liaison <liaison_at_[hidden]>
Sent: 12 November 2025 16:51
To: liaison_at_[hidden] <liaison_at_lists.isocpp.org>
Cc: Joshua Berne <berne_at_[hidden]>
Subject: Re: [isocpp-wg14/wg21-liaison] Extra arguments to va_start
On Wed, Nov 12, 2025 at 11:39 AM Jens Maurer via Liaison <liaison_at_[hidden]<mailto:liaison_at_[hidden]>> wrote:
On 11/12/25 16:51, Aaron Ballman via Liaison wrote:
> This is why I'm asking SG22 to step in; this seems like a needless incompatibility between C and C++.
>
> void func(int i, ...) {
> va_list list;
>
> va_start(list, blah); // #1
> va_start(list, __COUNTER__); // #2
> }
>
> #1 is required to be diagnosed in C17 and C++23, is required to be accepted in C++26. C23 is unclear IMO. The standard says the argument is not evaluated. So if the implementation expands to `sizeof(blah)` for some reason, that's allowed by the standard wording for va_start because `blah` is not evaluated?
Obviously. In sizeof(blah), "blah" is not evaluated,
but name lookup must succeed.
> I think the intent for C23 is clear in that this code is expected to be semantically well-formed regardless of what it expands to,
What is the context in which "semantically well-formed" is determined?
> and there's a recommendation to diagnose if any additional arguments are passed.
There are three levels of ignored:
(1) preprocessing tokens are ignored (__COUNTER__ is not incremented):
#define va_start(list, ...) __builtin_va_start(list)
(2) tokens are ignored (__COUNTER__ is incremented, no phase 7 name lookup):
#define va_start(list, ...) __builtin_va_start(list, __VA_ARGS__ ???)
(3) tokens are not evaluated (__COUNTER__ is incremented, phase 7 name lookup must succeed):
#define va_start(list, ...) ( sizeof((__VA_ARGS__, 0)) , __builtin_va_start(list) )
(4) tokens are evaluated as an expression:
#define va_start(list, ...) ( (__VA_ARGS__, 0), __builtin_va_start(list) )
It's easy to to (1) and probably easy to do (3) (even though the "sizeof"
might not be exactly the right machinery), but I don't know how to do (2)
with current technology, absent a new macro-like built-in.
(2) seems doable by stringizing the parameters and then discarding the resulting string.
Can someone speak to the intent in C23 ?
I thought (1) was intended to be a valid implementation in C23,
with QoI diagnostics for lookup errors on the second argument.
(Yes, __COUNTER__ would not be incremented.)
If (1) is not a valid implementation, we need to be a lot more
precise what we expect in terms of macro expansion and phase 7 name lookup.
> #2 is required to be diagnosed in C17 and C++23, is required to be accepted in C23 and C++26, where __COUNTER__ is expected to increment in C and expected to not increment in C++.
>
> In both cases, I think this boils down to whether the committees expect something like:
>
> #define va_start(list, ...) __builtin_va_start(list, __VA_ARGS__)
>
> where the optional arguments passed to the macro are expanded and must be valid for a call to a builtin function. I think this is how WG14 expected things to work and so #1 would be rejected due to use of an undeclared identifier, similar to what would happen with `1 || blah` in an expression (`blah` is not evaluated, but it still has to be semantically valid). And #2 would increment __COUNTER__ because the macro is expanded for the call to the builtin.
>
> Or whether the committees expect something more like:
>
> #define va_start(list, ...) __builtin_va_start(list)
>
> where the optional arguments passed to the macro are not expanded and therefore don't have to be valid beyond what the preprocessor requires. I think this is how WG21 expected things to work, so #1 would be accepted and #2 would not increment __COUNTER__.
I don't know if WG21 "expects" anything; https://cplusplus.github.io/LWG/issue4388
is phrased very much like an alignment with C23 that was overlooked when
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3348r4.pdf
rebased the C library parts of C++ onto C23.
Jens
_______________________________________________
Liaison mailing list
Liaison_at_[hidden]<mailto:Liaison_at_[hidden]socpp.org>
Subscription: https://lists.isocpp.org/mailman/listinfo.cgi/liaison
Link to this post: http://lists.isocpp.org/liaison/2025/11/1598.php
CAUTION: This email originated from outside of the organization. Do not click on links or open attachments unless you recognize the sender and know the content is safe.
This e-mail may contain information that is privileged or confidential. If you are not the intended recipient, please delete the e-mail and any attachments and notify us immediately.
Received on 2025-11-12 17:10:39
