Date: Wed, 12 Nov 2025 10:51:50 -0500
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? 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,
and there's a recommendation to diagnose if any additional arguments are
passed.
#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__.
~Aaron
On Tue, Nov 11, 2025 at 6:46 AM Alex Celeste via Liaison <
liaison_at_[hidden]> wrote:
> I believe we discussed basically this same question within WG14 before
> adopting the wording, and the "evaluated" wording was a revision/compromise
> intended to reduce the structness of the `va_start` macro so that it could
> still report errors if they were needed, but not to require anything
> special of it above the behaviour that would be expected for any other
> macro that doesn't exand its arguments into an evaluated position.
>
> So that second argument certainly isn't supposed to be being discarded by
> any special mechanism that would block incrementing `__COUNTER__` by some
> means not available to a user-defined macro. The original wording was
> stricter and might have implied this, but "evaluated" was chosen to
> indicate that it's the C-level evaluation that definitely doesn't happen,
> without implying anything about macro expansion. The macro part of this
> feature should just follow normal macro rules.
>
> Thanks,
>
> Alex
>
> [image: Perforce Software]
>
> Alex Celeste | Elder Witch of the C Compiler, Helix QAC
> <https://www.perforce.com/products/helix-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 J Decker
> via Liaison <liaison_at_[hidden]>
> *Sent:* 11 November 2025 10:53
> *To:* liaison_at_[hidden] <liaison_at_[hidden]>
> *Cc:* J Decker <d3ck0r_at_[hidden]>
> *Subject:* Re: [isocpp-wg14/wg21-liaison] Extra arguments to va_start
>
>
>
> On Mon, Nov 10, 2025 at 9:10 AM Aaron Ballman via Liaison <
> liaison_at_[hidden]> wrote:
>
> I think the situation with va_start is a bit confused and there
> appears to be implementation divergence in the wild. Consider this
> code:
>
> void func(int i, ...) {
> va_list list;
> va_start(list, +);
> }
>
> 7.16.2.5p4: Only the first argument passed to va_start is evaluated.
> If any additional arguments expand to include unbalanced parentheses,
> or a preprocessing token that does not convert to a token, the
> behavior is undefined.
>
>
> 'Is Evaluated' means 'computing the value of an expression and carrying
> out its associated side effects'
> The second argument, not being evaluated, doesn't mean it's ignored, but
> that it's a literal that gets stuffed into the result without evaluation.
>
> It doesn't say 'there's only one argument, and others are ignored' it says
> 'only the first is evaluated'
>
> like doing `&((struct thing *)0)->member ` to get offsetof, the actual
> pointer there isn't evaluated...
>
>
>
> So "list" is evaluated and "+" is not. Is the + then discarded as a
> token? So is this valid? GCC and TCC accept, Clang and SDCC reject:
> https://godbolt.org/z/Yf317Kxfx What if `+` was an undeclared
> identifier instead?
>
> (Keep in mind, we use the same "only ... is evaluated" in other
> circumstances where the resulting code is still expected to be
> semantically correct. e.g., `1 || undeclared_identifier` is still a
> constraint violation even though `undeclared_identifier` is not
> evaluated.)
>
> Now think about this in terms of __COUNTER__ from C2y, with this code:
>
> void func(int i, ...) {
> va_list list;
> va_start(list, __COUNTER__);
> static_assert(__COUNTER__ == 0);
> }
>
> Does that static assertion pass or fail? It fails in Clang and GCC,
> doesn't compile correctly in SDCC, and TCC it succeeds:
> https://godbolt.org/z/56qT9sarK At least with Clang and GCC, the
> implementation of the macro is to forward to a builtin function so
> that the builtin can generate appropriate diagnostics, so the
> __COUNTER__ macro is expanded. Avoiding that expansion but keeping the
> same level of QoI is a surprisingly large burden (we'd need a new
> builtin and way to count the number of arguments in __VA_ARGS__ which
> does not expand the tokens, I believe).
>
> I think this is an issue SG22 should take up (hence, I've CCed Nina)
> because the C++ wording is different from the C wording. In C++, the
> tokens (if any) are *discarded* explicitly
> (https://eel.is/c++draft/cstdarg.syn#1.2). I believe in C++, both of
> those examples are expected to compile without diagnostics.
>
> Thanks!
>
> ~Aaron
> _______________________________________________
> Liaison mailing list
> Liaison_at_[hidden]
> Subscription: https://lists.isocpp.org/mailman/listinfo.cgi/liaison
> Searchable archives: http://lists.isocpp.org/liaison/2025/11/index.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.
>
> _______________________________________________
> Liaison mailing list
> Liaison_at_[hidden]
> Subscription: https://lists.isocpp.org/mailman/listinfo.cgi/liaison
> Link to this post: http://lists.isocpp.org/liaison/2025/11/1596.php
>
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? 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,
and there's a recommendation to diagnose if any additional arguments are
passed.
#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__.
~Aaron
On Tue, Nov 11, 2025 at 6:46 AM Alex Celeste via Liaison <
liaison_at_[hidden]> wrote:
> I believe we discussed basically this same question within WG14 before
> adopting the wording, and the "evaluated" wording was a revision/compromise
> intended to reduce the structness of the `va_start` macro so that it could
> still report errors if they were needed, but not to require anything
> special of it above the behaviour that would be expected for any other
> macro that doesn't exand its arguments into an evaluated position.
>
> So that second argument certainly isn't supposed to be being discarded by
> any special mechanism that would block incrementing `__COUNTER__` by some
> means not available to a user-defined macro. The original wording was
> stricter and might have implied this, but "evaluated" was chosen to
> indicate that it's the C-level evaluation that definitely doesn't happen,
> without implying anything about macro expansion. The macro part of this
> feature should just follow normal macro rules.
>
> Thanks,
>
> Alex
>
> [image: Perforce Software]
>
> Alex Celeste | Elder Witch of the C Compiler, Helix QAC
> <https://www.perforce.com/products/helix-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 J Decker
> via Liaison <liaison_at_[hidden]>
> *Sent:* 11 November 2025 10:53
> *To:* liaison_at_[hidden] <liaison_at_[hidden]>
> *Cc:* J Decker <d3ck0r_at_[hidden]>
> *Subject:* Re: [isocpp-wg14/wg21-liaison] Extra arguments to va_start
>
>
>
> On Mon, Nov 10, 2025 at 9:10 AM Aaron Ballman via Liaison <
> liaison_at_[hidden]> wrote:
>
> I think the situation with va_start is a bit confused and there
> appears to be implementation divergence in the wild. Consider this
> code:
>
> void func(int i, ...) {
> va_list list;
> va_start(list, +);
> }
>
> 7.16.2.5p4: Only the first argument passed to va_start is evaluated.
> If any additional arguments expand to include unbalanced parentheses,
> or a preprocessing token that does not convert to a token, the
> behavior is undefined.
>
>
> 'Is Evaluated' means 'computing the value of an expression and carrying
> out its associated side effects'
> The second argument, not being evaluated, doesn't mean it's ignored, but
> that it's a literal that gets stuffed into the result without evaluation.
>
> It doesn't say 'there's only one argument, and others are ignored' it says
> 'only the first is evaluated'
>
> like doing `&((struct thing *)0)->member ` to get offsetof, the actual
> pointer there isn't evaluated...
>
>
>
> So "list" is evaluated and "+" is not. Is the + then discarded as a
> token? So is this valid? GCC and TCC accept, Clang and SDCC reject:
> https://godbolt.org/z/Yf317Kxfx What if `+` was an undeclared
> identifier instead?
>
> (Keep in mind, we use the same "only ... is evaluated" in other
> circumstances where the resulting code is still expected to be
> semantically correct. e.g., `1 || undeclared_identifier` is still a
> constraint violation even though `undeclared_identifier` is not
> evaluated.)
>
> Now think about this in terms of __COUNTER__ from C2y, with this code:
>
> void func(int i, ...) {
> va_list list;
> va_start(list, __COUNTER__);
> static_assert(__COUNTER__ == 0);
> }
>
> Does that static assertion pass or fail? It fails in Clang and GCC,
> doesn't compile correctly in SDCC, and TCC it succeeds:
> https://godbolt.org/z/56qT9sarK At least with Clang and GCC, the
> implementation of the macro is to forward to a builtin function so
> that the builtin can generate appropriate diagnostics, so the
> __COUNTER__ macro is expanded. Avoiding that expansion but keeping the
> same level of QoI is a surprisingly large burden (we'd need a new
> builtin and way to count the number of arguments in __VA_ARGS__ which
> does not expand the tokens, I believe).
>
> I think this is an issue SG22 should take up (hence, I've CCed Nina)
> because the C++ wording is different from the C wording. In C++, the
> tokens (if any) are *discarded* explicitly
> (https://eel.is/c++draft/cstdarg.syn#1.2). I believe in C++, both of
> those examples are expected to compile without diagnostics.
>
> Thanks!
>
> ~Aaron
> _______________________________________________
> Liaison mailing list
> Liaison_at_[hidden]
> Subscription: https://lists.isocpp.org/mailman/listinfo.cgi/liaison
> Searchable archives: http://lists.isocpp.org/liaison/2025/11/index.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.
>
> _______________________________________________
> Liaison mailing list
> Liaison_at_[hidden]
> Subscription: https://lists.isocpp.org/mailman/listinfo.cgi/liaison
> Link to this post: http://lists.isocpp.org/liaison/2025/11/1596.php
>
Received on 2025-11-12 15:52:08
