How do I construct a "signed size_t" for use with scanf("%zn")?

247 Views Asked by At

I tried to obtain the number of characters read as a size_t, using this program:¹

#include <stdio.h>

int main(void)
{
    size_t i;
    sscanf("abc", "%*s%zn", &i);
    printf("%zu", i);
}

GCC 12 warns about this:

scanf.c: In function ‘main’:
scanf.c:7:25: warning: format ‘%zn’ expects argument of type ‘signed size_t *’, but argument 3 has type ‘size_t *’ {aka ‘long unsigned int *’} [-Wformat=]
    7 |     sscanf("abc", "%*s%zn", &i);
      |                       ~~^   ~~
      |                         |   |
      |                         |   size_t * {aka long unsigned int *}
      |                         long int *
      |                       %ln

And it's correct² do do so; in the C17 draft standard, page 234, we read (my emphasis)

No input is consumed. The corresponding argument shall be a pointer to signed integer into which is to be written the number of characters read from the input stream so far by this call to the fscanf function.

Earlier standards contain similar wording.

So how do I (portably) create a signed equivalent of size_t for this conversion?

In C++, I could use std::make_signed_t<std::size_t>, but that's obviously not an option for C code. Without that, it seems that %zn conversion is unusable in C.


¹ The real-world case from which this arises came from reviewing Simple photomosaic generator, where we wanted a more general form of strto(), and so need %n to determine the end of conversion. I know we can use plain int for all here, but wanted to check the expected behaviour before reporting as a bug.

² Other than calling the required type signed size_t *, which is obviously not a valid C type name.

2

There are 2 best solutions below

3
chux - Reinstate Monica On BEST ANSWER

How do I construct a "signed size_t" for use with scanf("%zn")?

You are out of luck. As of C17

z Specifies that a following d, i, o, u, x, X, or n conversion specifier applies to an argument with type pointer to size_t or the corresponding signed integer type. C17dr § 7.21.6.2 11

n No input is consumed. The corresponding argument shall be a pointer to signed integer .... C17dr § 7.21.6.2 12

And size_t is an unsigned type.

Yet C never details how to make the corresponding signed integer type.

There is no specified signed type corresponding to size_t in standard C.


Alternatives:

  • Live with "%n", &int_object and then size_t i = (unsigned) int_object;. (Cast important)

  • Use "%jn", &intmax_t_object and then size_t i = (size_t) intmax_t_object;.


If pressed to typedef a signed_size_t, the following should portably work, but you are on your own.

#include <assert.h>
#include <inttypes.h>
#include <limits.h>
#include <stddef.h>
#include <stdint.h>

#if SIZE_MAX == UINT_MAX
  typedef int signed_size_t;
#elif SIZE_MAX == ULONG_MAX
  typedef long signed_size_t;
#elif SIZE_MAX == ULLONG_MAX
  typedef long long signed_size_t;
#elif SIZE_MAX == UINTMAX_MAX;
  typedef intmax_t signed_size_t;
#elif SIZE_MAX == USHRT_MAX
  typedef short signed_size_t;
#else
  #error Strange `size_t`.
#endif

_Static_assert(sizeof(size_t) == sizeof(signed_size_t), "Strange size_t");

About non-standard ssize_t

ssize_t is not a certain match for the corresponding signed integer type.

Even The Open Group Base Specifications Issue 7, 2018 edition has:

ssize_t
This is intended to be a signed analog of size_t. The wording is such that an implementation may either choose to use a longer type or simply to use the signed version of the type that underlies size_t. ...


Similar question How to use "zd" specifier with printf()?.

5
Andrew Henle On

"signed size_t" doesn't really exist as a standard-defined type, in either POSIX or any version of the C standard.

POSIX ssize_t is specified as "The type ssize_t shall be capable of storing values at least in the range [-1, {SSIZE_MAX}].", so even if ssize_t is available, it's not strictly suitable for use as a "signed size_t" value.

While there's no type that's guaranteed to be large enough to hold a "signed size_t" type of value in strictly-conforming code, on all platforms that I'm aware of, the standard type long long int seems to me to be the most "future proof". (The odds of size_t being larger than unsigned long long int are IMO likely somewhere between zero and infinitesimal, but I don't think that's precluded by any version of the C standard.)

As I write this, no platform that I'm aware of uses a size_t value larger than 64 bits, and long long int is guaranteed to be at least 64 bits, so it will be large enough.

int64_t seems like it would be a good choice, but should a system ever have a size_t larger than 64 bits, int64_t would fail. I suspect that any system implementation with size_t larger than 64 bits would likely expand long long int to match.

You could always add something like this to your code to protect yourself

#if SIZE_MAX > ULLONG_MAX
#error size_t larger than unsigned long long int
#endif

Nevermind it's easier to use the " %lld ..." format specifier to scan a long long int than it is to use the " " SCNd64 " ..." macro to scan an int64_t...

If you really want to create a "matching signed size_t" type, you could expand on using SIZE_MAX in a #if ladder, comparing it to various U*MAX values to find the "most correct" matching signed integer type to use in a typedef. But then you'd have to create your own scanf() (and maybe printf() macros).

OR...

Or you could just pedantically play with fire and use ssize_t on POSIX systems, and create your own ssize_t on non-POSIX systems. IME there are no ssize_t implementations that don't de facto act as a "signed size_t" value.