nullptr passed to a variadic function in C++ now converted to void*, per CWG722
va_arg on array types and half precision floats now diagnosed.
Details
- Reviewers
aaron.ballman
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
There is one observable difference by explicitly casting nullptr_t to void*: prvalues of type nullptr_t aren't read from (which seemed unintentional):
static void* f(int x, ...) { __builtin_va_list args; __builtin_va_start(args, x); void* arg = __builtin_va_arg(args, void*); __builtin_va_end(args); return arg; } int main() { int x; void* x_ptr = &x; void* result = f(0, __builtin_bit_cast(decltype(nullptr), x_ptr)); __builtin_printf("%p\n%p\n%p\n", x_ptr, result, nullptr); }
https://godbolt.org/z/Pfv8Wbhxj
An object of type nullptr_t is passed to the function, but when it is read with void* arg = __builtin_va_arg(args, void*);, it is not a null pointer.
clang/test/Sema/format-strings-pedantic.c | ||
---|---|---|
20 | In C2x, nullptr passed as an argument and retrieved via va_arg(ap, void *) is valid (See C2x 7.16.1.1p2) and this is the same as printf("%p", (void*) nullptr), but this should still be fine as a "pedantic" warning | |
clang/www/cxx_dr_status.html | ||
4376 | It may be better to mark this as "Yes", since in old clang versions passing nullptr would work by virtue of it having representation of a null void* pointer, and it only affected diagnostics |
clang/lib/Sema/SemaExpr.cpp | ||
---|---|---|
17377 | This warns if you call va_arg(ap, double[2]). However this might be valid since the actual argument only has to be a "compatible type" and I think double _Complex is compatible with double[2]. I think we should warn anyways, since array rvalues are tricky to work with, and the user probably passed a double[2] and should retrieve the double*. |
Thank you for working on this! FWIW, precommit Ci found a relevant test failure that should also be addressed.
clang/lib/Sema/SemaExpr.cpp | ||
---|---|---|
1069–1080 | Oh fun, C and C++ solved this issue differently. In C++, there's an implicit conversion so the nullptr_t is converted to void * on the call, and in C there's an allowance for va_arg to handle nullptr_t as a void *. And without further changes, C++ picks that rule up automatically once it rebases onto C23: http://eel.is/c++draft/support#cstdarg.syn-1 | |
17377 | _Complex double and double[2] are not compatible types in C. C2x 6.2.7p1: Two types are compatible types if they are the same. Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.6 for declarators. ... (The rest is talking about structures, enumerations, and unions). _Complex is a type specifier, so.... C2x 6.7.2p7: Each of the comma-separated multisets designates the same type, except that for bit-fields, it is implementation-defined whether the specifier int designates the same type as signed int or the same type as unsigned int. (The multisets it describes do not include any array types.) | |
clang/test/Sema/format-strings-pedantic.c | ||
20 | I don't think this should be diagnosed pedantically; the code is correct. |
C99 7.15.1.1p2 explicitly allows a char* to be retrieved as a void* with va_arg, so the -Wformat-pedantic warning was incorrect.
Precommit CI found a related issue that should be addressed.
clang/www/cxx_dr_status.html | ||
---|---|---|
4376 | I think it's better to use Clang 18, otherwise people will think there's a diagnostic bug with earlier versions. We usually use "Yes" to mean "we've always supported this correctly, as best we can tell". |
Fix scanf("%p", (char**)ptr) for -Wformat-pedantic
Also added more tests for %p and -Wformat
clang/test/Sema/format-pointer.c | ||
---|---|---|
40 | Should this be a pedantic warning? If this reads a non-null pointer, the bit pattern of np will be messed up but np will still read as a nullptr. |
clang/lib/AST/FormatString.cpp | ||
---|---|---|
484–487 | We can simplify a bit further these days. | |
521–526 | Fixes to match the usual coding style rules. Wow, Phab's diff engine struggles with that edit. Basically: no else after a return, remove braces on single-line if, renamed to PointeeTy. | |
clang/lib/Sema/SemaExpr.cpp | ||
1069–1079 | I think this code should live in Sema::DefaultArgumentPromotion() because it's related specifically to default argument promotions: https://eel.is/c++draft/expr#call-11.sentence-5 | |
17374–17375 | Hmmm... the existing code seems wrong to me because it's not paying any attention to FLT_EVAL_METHOD, but I think it probably should? CC @jcranmer-intel @zahiraam for opinions. Actually, I wonder if the correct approach here is to split Sema::DefaultArgumentPromotion() up so that we can calculate what the default argument promoted type is of the expression independent of performing the actual promotion, and call the promotion type calculation logic here? | |
clang/test/Sema/format-pointer.c | ||
40 | That's a fun scenario, good test case! C23 7.26.6.2p10: Except in the case of a % specifier, the input item (or, in the case of a %n directive, the count of input characters) is converted to a type appropriate to the conversion specifier. If the input item is not a matching sequence, the execution of the directive fails: this condition is a matching failure. Unless assignment suppression was indicated by a *, the result of the conversion is placed in the object pointed to by the first argument following the format argument that has not already received a conversion result. If this object does not have an appropriate type, or if the result of the conversion cannot be represented in the object, the behavior is undefined. p12, the p specifier: Matches an implementation-defined set of sequences, which should be the same as the set of sequences that may be produced by the %p conversion of the fprintf function. The corresponding argument shall be a pointer to a pointer of void. The input item is converted to a pointer value in an implementation-defined manner. If the input item is a value converted earlier during the same program execution, the pointer that results shall compare equal to that value; otherwise the behavior of the %p conversion is undefined. The corresponding argument is not a pointer to a pointer of void, so it's UB, not just pedantically so. |
Address feedback; Reuse DefaultArgumentPromotion instead of duplicating its functionality when building VaArgExpr
There is the added bonus for it working with dependent types somewhat (template<typename T> __builtin_va_arg(ap, T[2]); will warn in the template instead of waiting for it to be instantiated).
I can add the check for dependent types back if this is unwanted.
I don't think there are any edge cases where this will give a false warning.
clang/lib/Sema/SemaExpr.cpp | ||
---|---|---|
17374–17375 | C23 6.5.2.2p6 [draft N3096] says "trailing arguments that have type float are promoted to double". FLT_EVAL_METHOD shouldn't need to apply here, since it's largely about reflecting that things like x87 using 80-bit precision internally, and not actual argument passing. |
Precommit CI still has a related failure, it seems.
clang/lib/Sema/SemaExpr.cpp | ||
---|---|---|
17374–17375 | Ah that's good to know, thank you! |
Use C's definition of type compatibility even in C++ mode (to not warn about enums promoted to their underlying types)
clang/test/SemaCXX/varargs.cpp | ||
---|---|---|
34 | Unscoped1 is promoted to int when passed to a variadic function. The underlying type for Unscoped1 is unsigned int, so only Unscoped1 and unsigned int are compatible, not Unscoped1 and int. An Unscoped1 passed to a variadic function must be retrieved via va_arg(ap, int). |
clang/test/SemaCXX/varargs.cpp | ||
---|---|---|
34 | Although I guess the warning is now wrong because even though void f(int x, ...) { std::va_list ap; va_start(ap, x); va_arg(ap, Unscoped1); } f(0, Unscoped1{2}) would be UB, f(0, 2u) would not be UB. The user still should be warned about it, so I could create a new warning "second argument to 'va_arg' is of promotable enumeration type 'Unscoped1'; this va_arg may have undefined behavior because arguments of this enumeration type will be promoted to 'int', not the underlying type 'unsigned int'", and maybe suggest a fix Unscoped1{va_arg(ap, unsigned)}. Or we could ignore it and pretend that int and enums with underlying types unsigned are compatible for the purposes of va_arg |
clang/test/SemaCXX/varargs.cpp | ||
---|---|---|
34 | I think we shouldn't warn in this case because of C23 7.16.1.1p2:
Coupled with C23 6.7.2.2p13:
WDYT? |
enums now considered compatible with unsigned/signed versions of their underlying type for the purposes of -Wvarargs
clang/test/SemaCXX/varargs.cpp | ||
---|---|---|
34 | This seems to have changed very recently between N3096 (April C23 draft) and N3149 (July C23 draft) by N3112. For reference, the old wording (also in C17) read:
So with the current rules, yes they would be compatible and this shouldn't warn. I've changed it so it checks types with corresponding signedness. |
I think this is looking close to good, just had some minor nits and an extra test case to consider.
clang/lib/Sema/SemaExpr.cpp | ||
---|---|---|
17255 | ||
17264–17268 | This code is correct, but let's add an explicit test for the subtle edge case with _Atomic types. getUnqualifiedType() does not strip atomic qualifiers, so int and _Atomic int should remain incompatible. | |
clang/test/Sema/format-pointer.c | ||
2–9 | ||
clang/test/SemaCXX/varargs.cpp | ||
34 |
Yes, this changed during ballot comment resolution at the June 2023 meeting; it was US-127 (see https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3148.doc for the paper trail)
Thank you! |
We can simplify a bit further these days.