How to get SIZE_MAX in C89

2020-04-08 13:48发布

I'm trying to get SIZE_MAX in C89.

I thought of the following way to find SIZE_MAX:

const size_t SIZE_MAX = -1;

Since the standard (§6.2.1.2 ANSI C) says:

When a signed integer is converted to an unsigned integer with equal or greater size, if the value of the signed integer is nonnegative, its value is unchanged. Otherwise: if the unsigned integer has greater size, the signed integer is first promoted to the signed integer corresponding to the unsigned integer; the value is converted to unsigned by adding to it one greater than the largest number that can be represented in the unsigned integer type 28

With footnote 28:

In a two's-complement representation, there is no actual change in the bit pattern except filling the high-order bits with copies of the sign bit if the unsigned integer has greater size.

This seems like this has defined behavior, but I'm not quite sure if I understand the wording of that paragraph correctly.

Note that this question is explicitly about C89, so this doesn't answer my question because the standard has different wording.

If that doesn't work, the other way I came up with is:

size_t get_size_max() {
    static size_t max = 0;
    if (max == 0) {
        max -= 1U;
    }

    return max;
}

But I couldn't find anything about unsigned integer underflow in the standard, so I'm poking in the dark here.

标签: c c89 size-t
2条回答
再贱就再见
2楼-- · 2020-04-08 14:24

I recommend using the macro definition as described in M.M's answer.

In some cases, you might need a similar macro, but as a numerical constant, so that you can use it in preprocessor directives like #if VALUE > 42 ... #endif. I commented that in such cases, a helper program can be run at compile time, to compute and print a header file defining such constants.

Obviously, this will not work when cross-compiling to a different architecture; in that case, the header file must be provided by some other way. (For example, the project could have a subdirectory of pre-generated headers, and a list of known architectures for each, so that the user can simply copy the header file into place.)

Creating a Makefile and associated facilities for running such programs (and only if the user did not copy the header file into place), is not difficult.

First, let's say your program consists of two source files, foo.c:

#include <stdlib.h>
extern void hello(void);

int main(void)
{
    hello();
    return EXIT_SUCCESS;
}

and a bar.c:

#include <stdio.h>
#include "size_max.h"

#define  STRINGIFY_(s) #s
#define  STRINGIFY(s) STRINGIFY_(s)

void hello(void)
{
    fputs("SIZE_MAX = \"" STRINGIFY(SIZE_MAX) "\".\n", stdout);
}

The above bar.c converts the SIZE_MAX preprocessor macro to a string, and prints it. If we had #define SIZE_MAX (size_t)(-1), it would print SIZE_MAX = "(size_t)(-1)".

Note that bar.c includes file size_max.h, which we do not have. This is the header file we intend to generate using our helper program, size_max.c:

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    printf("#ifndef SIZE_MAX\n");
    printf("#define SIZE_MAX %lluU\n", (unsigned long long)(size_t)(-1));
    printf("#endif\n");
    return EXIT_SUCCESS;
}

chux noted in a comment that u suffix (for sufficiently large unsigned integer type) might be necessary. If that is not what you require, I'm sure you can modify the macro generator helper to suit your needs.

M.M noted in a comment that %z is not supported by ANSI C/ISO C90, so the above program first creates the constant using (size_t)(-1), then casts and prints it in the unsigned long long format.

Now, Makefiles can be written in an OS-agnostic manner, but I'm too lazy to do that here, so I shall use the values that work with GNU tools. To make it work on other systems, you only need to modify the values of

  • CC, to reflect the compiler you use

  • CFLAGS, to reflect your preferred compiler options

  • LD, to reflect your linker, unless the same as CC

  • LDFLAGS, if you need some linker flags (maybe -lm?)

  • RM, to reflect the command to delete unnecessary files

  • File names, if your build system requires some funky file name extension for executables

Anyway, here's the Makefile:

CC      := gcc
CFLAGS  := -Wall -O2
LD      := $(CC)
LDFLAGS := $(CFLAGS)
RM      := rm -f

# Programs to be built
PROGS   := example

# Relative path to use for executing the header generator helper program
HEADERGEN := ./headergen

# Rules that do not correspond to actual files
.PHONY: all clean headergen

# Default rule is to build all binaries
all: $(PROGS)

# Clean rule removes build files and binaries
clean:
    -$(RM) $(PROGS) $(HELPROG) *.o size_max.h

# Rule to "rebuild" size_max.h
size_max.h: size_max.c
    -@$(RM) $(HEADERGEN) size_max.h
    @$(CC) $(CFLAGS) $^ -o $(HEADERGEN)
    $(HEADERGEN) > size_max.h
    @$(RM) $(HEADERGEN)

# Rule to build object files from .c source files
%.o: %.c size_max.h
    $(CC) $(CFLAGS) -c $<

# Example binary requires foo.o and bar.o:
example: foo.o bar.o size_max.h
    $(LD) $(LDFLAGS) foo.o bar.o -o $@

Note that the indentation should use tabs, not spaces, so if you copy-paste the above, run e.g. sed -e 's|^ *|\t|' -i Makefile to fix it.

Before zipping or tarring the source tree, run make clean to remove any generated files from it.

Note the extra size_max.h in the recipe prerequisites. It tells make to ensure that size_max.h exists before it can complete the recipe.

The downside of this approach is that you cannot use $^ in link recipes to refer to all prerequisite file names. $< refers to the first prerequisite file name. If you use GNU make or a compatible make, you can use $(filter-out %.h, %^) (to list all prerequisites except for header files), though.

If all your binaries are built from a single source with the same name, you can replace the last two recipes with

# All programs are built from same name source files:
$(PROGS): %: %.c size_max.h
    $(CC) $(CFLAGS) $< $(LDFLAGS) -o $@

On my system, running

make clean all && ./example

outputs

rm -f example  *.o size_max.h
./headergen > size_max.h
gcc -Wall -O2 -c foo.c
gcc -Wall -O2 -c bar.c
gcc -Wall -O2 foo.o bar.o -o example
SIZE_MAX = "18446744073709551615U".

and running

make CC="gcc-5" CFLAGS="-Wall -std=c99 -pedantic -m32" clean all && ./example

outputs

rm -f example  *.o size_max.h
./headergen > size_max.h
gcc-5 -Wall -std=c99 -pedantic -m32 -c foo.c
gcc-5 -Wall -std=c99 -pedantic -m32 -c bar.c
gcc-5 -Wall -std=c99 -pedantic -m32 foo.o bar.o -o example
SIZE_MAX = "4294967295U".

Note that make does not detect if you change compiler options, if you edit the Makefile or use different CFLAGS= or CC= options when running make, so you do need then specify the clean target first, to ensure you start from a clean slate with the new settings in effect.

During normal editing and builds, when you don't change compilers or compiler options, there is no need to make clean between builds.

查看更多
家丑人穷心不美
3楼-- · 2020-04-08 14:27

You could use:

#ifndef SIZE_MAX
#define SIZE_MAX ((size_t)(-1))
#endif

The behaviour of converting -1 to unsigned integer type is defined under section C11 6.3.1.3 "Conversions - Signed and unsigned integers". C89 had an equivalent definition, numbered 3.2.1.2. In fact you quoted the ISO C90 definition 6.2.1.2 in your question (the difference between ANSI C89 and ISO C90 is that the sections are numbered differently).

I would not recommend using a const variable, since they cannot be used in constant expressions.


Note: This can't be used in C90 preprocessor arithmetic, which only works on integer constant expressions that contain no casts or words, so we can't use any sizeof tricks. In that case you might need a system-specific definition; there's no standard way for the preprocessor to detect a typedef.

查看更多
登录 后发表回答