5
votes

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.

2
There is plenty about unsigned integer overflow in the standard. Essentially it uses modulo arithmetic. - Peter
@Peter: Well I didn't find it. The word underflow is used 4 times in the copy that I have and none of it is about unsigned types. Also modulo arithmetic is never mentioned, just the word "modulo". And normal modulo isn't necessarily defined for negative values. - FSMaxB
@FSMaxB - you haven't read closely enough and the standards don't spoonfeed like you are expecting. The 1999 C standard, section 6.2.5, second sentence of para 9. "A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.". That describes the mathematical notion of modulo arithmetic. I don't have the 1989 standard handy right now, but it definitely has words with the same net meaning. - Peter
SIZE_MAX is not necessarily the same thing as ~(size_t)0 nor (size_t)-1, so the assumption that you can 'determine' it is flawed from the start. SIZE_MAX is something set by the compiler. See this. - Lundin

2 Answers

8
votes

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.

2
votes

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.