What type is used in C++ to define an array size?

Viewed 2067

Compiling some test code in avr-gcc for an 8-bit micro-controller, the line

const uint32_t N = 65537;
uint8_t values[N]; 

I got the following compilation warning (by default should be an error, really)

warning: conversion from 'long unsigned int' to 'unsigned int' changes value from '65537' to '1' [-Woverflow]
uint8_t values[N];

Note that when compiling for this target, sizeof(int) is 2.

So it seems that, at an array size cannot exceed the size of an unsigned int.

Am I correct? Is this GCC-specific or is it part of some C or C++ standard?

Before somebody remarks that an 8-bit microcontroller generally does not have enough memory for an array so large, let me just anticipate saying that this is beside the point.

6 Answers

size_t is considered as the type to use, despite not being formally ratified by either the C or C++ standards.

The rationale for this is that the sizeof(values) will be that type (that is mandatated by the C and C++ standards), and the number of elements will be necessarily not greater than this since sizeof for an object is at least 1.

So it seems that, at an array size cannot exceed the size of an unsigned int.

That seems to be the case in your particular C[++] implementation.

Am I correct? Is this gcc-specific or is it part of some C or C++ standard?

It is not a characteristic of GCC in general, nor is it specified by either the C or C++ standard. It is a characteristic of your particular implementation: a version of GCC for your specific computing platform.

The C standard requires the expression designating the number of elements of an array to have an integer type, but it does not specify a particular one. I do think it's strange that your GCC seems to claim it's giving you an array with a different number of elements than you specified. I don't think that conforms to the standard, and I don't think it makes much sense as an extension. I would prefer to see it reject the code instead.

I'll dissect the issue with the rules in the "incorrekt and incomplet" ISO CPP standard draft n4659. Emphasis is added by me.

11.3.4 defines array declarations. Paragraph one contains

If the constant-expression [between the square brackets] (8.20) is present, it shall be a converted constant expression of type std::size_t [...].

std::size_t is from <cstddef>and defined as

[...] an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object.

Since it is imported via the C standard library headers the C standard is relevant for the properties of size_t. The ISO C draft N2176 prescribes in 7.20.3 the "minimal maximums", if you want, of integer types. For size_t that maximum is 65535. In other words, a 16 bit size_t is entirely conformant.

A "converted constant expression" is defined in 8.20/4:

A converted constant expression of type T is an expression, implicitly converted to type T, where the converted expression is a constant expression and the implicit conversion sequence contains only [any of 10 distinct conversions, one of which concerns integers (par. 4.7):]

— integral conversions (7.8) other than narrowing conversions (11.6.4)

An integral conversion (as opposed to a promotion which changes the type to equivalent or larger types) is defined as follows (7.8/3):

A prvalue of an integer type can be converted to a prvalue of another integer type.

7.8/5 then excludes the integral promotions from the integral conversions. This means that the conversions are usually narrowing type changes.

Narrowing conversions (which, as you'll remember, are excluded from the list of allowed conversions in converted constant expressions used for array sizes) are defined in the context of list-initialization, 11.6.4, par. 7

A narrowing conversion is an implicit conversion
[...]
7.31 — from an integer type [...] to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type.

This is effectively saying that the effective array size must be the constant value at display, which is an entirely reasonable requirement for avoiding surprises.


Now let's cobble it all together. The working hypothesis is that std::size_t is a 16 bit unsigned integer type with a value range of 0..65535. The integer literal 65537 is not representable in the system's 16 bit unsigned int and thus has type long. Therefore it will undergo an integer conversion. This will be a narrowing conversion because the value is not representable in the 16 bit size_t2, so that the exception condition in 11.6.4/7.3, "value fits anyway", does not apply.

So what does this mean?

11.6.4/3.11 is the catch-all rule for the failure to produce an initializer value from an item in an intializer list. Because the initializer-list rules are used for array sizes, we can assume that the catch-all for conversion failure applies to the array size constant:

(3.11) — Otherwise, the program is ill-formed.

A conformant compiler is required to produce a diagnostic, which it does. Case closed.


1 Yes, they sub-divide paragraphs.

2 Converting an integer value of 65537 (in whatever type can hold the number — here probably a `long) to a 16 bit unsigned integer is a defined operation. 7.8/2 details:

If the destination type is unsigned, the resulting value is the least unsigned integer congruent to the source integer (modulo 2n where n is the number of bits used to represent the unsigned type). [ Note: In a two’s complement representation, this conversion is conceptual and there is no change in the bit pattern (if there is no truncation). —end note ]

The binary representation of 65537 is 1_0000_0000_0000_0001, i.e. only the least significant bit of the lower 16 bits is set. The conversion to a 16 bit unsigned value (which circumstantial evidence indicates size_t is) computes the [expression value] modulo 2^16, i.e. simply takes the lower 16 bits. This results in the value of 1 mentioned in the compiler diagnostics.

In your implementation size_t is defined as unsigned int and uint32_t is defined as a long unsigned int. When you create a C array the argument for the array size gets implicitly converted to size_t by the compiler.

This is why you're getting a warning. You're specifying the array size argument with an uint32_t that gets converted to size_t and these types don't match.

This is probably not what you want. Use size_t instead.

The value returned by sizeof will be of type size_t.

It is generally used as the number of elements in an array, because it will be of sufficient size. size_t is always unsigned but it is implementation-defined which type this is. Lastly, it is implementation-defined whether the implementation can support objects of even SIZE_MAX bytes... or even close to it.

[This answer was written when the question was tagged with C and C++. I have not yet re-examined it in light of OP’s revelation they are using C++ rather than C.]

size_t is the type the C standard designates for working with object sizes. However, it is not a cure-all for getting sizes correct.

size_t should be defined in the <stddef.h> header (and also in other headers).

The C standard does not require that expressions for array sizes, when specified in declarations, have the type size_t, nor does it require that they fit in a size_t. It is not specified what a C implementation ought to do when it cannot satisfy a request for an array size, especially for variable length arrays.

In your code:

const uint32_t N = 65537;
uint8_t values[N];

values is declared as a variable length array. (Although we can see the value of N could easily be known at compile time, it does not fit C’s definition of a constant expression, so uint8_t values[N]; qualifies as a declaration of a variable length array.) As you observed, GCC warns you that the 32-bit unsigned integer N is narrowed to a 16-bit unsigned integer. This warning is not required by the C standard; it is a courtesy provided by the compiler. More than that, the conversion is not required at all—since the C standard does not specify the type for an array dimension, the compiler could accept any integer expression here. So the fact that it has inserted an implicit conversion to the type it needs for array dimensions and warned you about it is a feature of the compiler, not of the C standard.

Consider what would happen if you wrote:

size_t N = 65537;
uint8_t values[N];

Now there would be no warning in uint8_t values[N];, as a 16-bit integer (the width of size_t in your C implementation) is being used where a 16-bit integer is needed. However, in this case, your compiler likely warns in size_t N = 65537;, since 65537 will have a 32-bit integer type, and a narrowing conversion is performed during the initialization of N.

However, the fact that you are using a variable length array suggests you may be computing array sizes at run-time, and this is only a simplified example. Possibly your actual code does not use constant sizes like this; it may calculate sizes during execution. For example, you might use:

size_t N = NumberOfGroups * ElementsPerGroup + Header;

In this case, there is a possibility that the wrong result will be calculated. If the variables all have type size_t, the result may easily wrap (effectively overflow the limits of the size_t type). In this case, the compiler will not give you any warning, because the values are all the same width; there is no narrowing conversion, just overflow.

Therefore, using size_t is insufficient to guard against errors in array dimensions.

An alternative is to use a type you expect to be wide enough for your calculations, perhaps uint32_t. Given NumberOfGroups and such as uint32_t types, then:

const uint32_t N = NumberOfGroups * ElementsPerGroup + Header;

will produce a correct value for N. Then you can test it at run-time to guard against errors:

if ((size_t) N != N)
    Report error…

uint8_t values[(size_t) N];
Related