17

In C++, size_t (or, more correctly T::size_type which is "usually" size_t; i.e., a unsigned type) is used as the return value for size(), the argument to operator[], etc. (see std::vector, et. al.)

On the other hand, .NET languages use int (and, optionally, long) for the same purpose; in fact, CLS-compliant languages are not required to support unsigned types.

Given that .NET is newer than C++, something tells me that there may be problems using unsigned int even for things that "can't possibly" be negative like an array index or length. Is the C++ approach "historical artifact" for backwards compatibility? Or are there real and significant design tradeoffs between the two approaches?

Why does this matter? Well ... what should I use for a new multi-dimensional class in C++; size_t or int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};
3

4 Answers 4

10

Given that .NET is newer than C++, something tells me that there may be problems using unsigned int even for things that "can't possibly" be negative like an array index or length.

Yes. For certain types of applications such as image processing or array processing, it is often necessary to access elements relative to the current position:

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

In these types of applications, you cannot perform range check with unsigned integers without thinking carefully:

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Instead you have to rearrange your range check expression. That is the main difference. Programmers must also remember the integer conversion rules. When in doubt, re-read http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

A lot of applications do not need to use very large array indices, but they do need to perform range checks. Furthermore, a lot of programmers are not trained to do this expression rearrangement gymnastics. A single missed opportunity opens the door to an exploit.

C# is indeed designed for those applications that will not need more than 2^31 elements per array. For example, a spreadsheet application does not need to deal with that many rows, columns, or cells. C# deals with the upper limit by having optional checked arithmetic that can be enabled for a block of code with a keyword without messing with compiler options. For this reason, C# favors the use of signed integer. When these decisions are considered altogether, it makes good sense.

C++ is simply different, and is harder to get correct code.

Regarding the practical importance of allowing signed arithmetic to remove a potential violation of "principle of least astonishment", a case in point is OpenCV, which uses signed 32-bit integer for matrix element index, array size, pixel channel count, etc. Image processing is an example of programming domain that uses relative array index heavily. Unsigned integer underflow (negative result wrapped around) will severely complicate algorithm implementation.

3
14

This answer truly depends on who is going to use your code, and what standards they want to see.

size_t is an integer size with a purpose:

The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object. (C++11 specification 18.2.6)

Thus, any time you wish to work with the size of objects in bytes, you should use size_t. Now in many cases, you're not using these dimensions/indexes to count bytes, but most developers choose to use size_t there for consistency.

Note that you should always use size_t if your class is intended to have the look and feel of a STL class. All of the STL classes in the specification use size_t. It is valid for the compiler to typedef size_t to be unsigned int, and it's also valid for it to be typedefed to unsigned long. If you use int or long directly, you'll eventually run into compilers where a person who thinks your class followed the STL's style gets trapped because you didn't follow the standard.

As for using signed types, there's a few advantages:

  • Shorter names -- it's really easy for people to type int, but much harder to clutter the code with unsigned int.
  • One integer for each size -- There is only one CLS compliant integer of 32-bits, which is Int32. In C++, there's two (int32_t and uint32_t). This can make API interoperability simpler

The big disadvantage of signed types is the obvious one: you lose half of your domain. A signed number cannot count as high as an unsigned number. When C/C++ came around, this was very important. One needed to be able to address the full capability of the processor, and to do that you needed to use unsigned numbers.

For the kinds of applications .NET targeted, there was not as strong of a need for a full-domain unsigned index. Many of the purposes for such numbers are simply invalid in a managed language (memory pooling comes to mind). Also, as .NET came out, 64-bit computers were clearly the future. We're a long way away from needing the full range of a 64-bit integer, so sacrificing one bit is not as painful as it was before. If you really need 4 billion indexes, you simply switch to using 64-bit integers. At worst, you run it on a 32 bit machine and it's a little slow.

I view the trade as one of convenience. If you happen to have enough computing power that you don't mind wasting a bit of your index type that you will never-ever-ever use, then it's convenient to just type int or long and walk away from it. If you find you really wanted that last bit, then you probably should have paid attention to the signedness of your numbers.

2
  • 5
    @Dan You can construct cases like that where having unsigned ints would matter, and in those cases its best to use the full language features to resolve it. However, I must say that it would be an interesting construction to have a class where bar_ * baz_ can overflow a signed integer but not an unsigned integer. Limiting ourselves to C++, it's worth noting that unsigned overflow is defined in the spec, but signed overflow is undefined behavior, so if the modulo arithmetic of unsigned integers is desirable, definitely use them, because its actually defined!
    – Cort Ammon
    Commented Dec 13, 2016 at 21:55
  • 1
    @Dan - if the size() overflowed the signed multiplication, you're in language UB land. (and in fwrapv mode, see next:) When then, with just a tiny wee bit more, it overflowed the unsigned multiplication, you in user-code-bug land - you would return a bogus size. So I don't think unsigned buys much here.
    – Martin Ba
    Commented Dec 14, 2016 at 21:50
5

I think the answer of rwong above already excellently highlights the issues.

I'll add my 002:

  • size_t, that is, a size that ...

    can store the maximum size of a theoretically possible object of any type (including array).

    ... is only required for range indices when sizeof(type)==1, that is, if you're dealing with byte (char) types. (But, we note, it can be smaller than a ptr type:

  • As such, xxx::size_type could be used in 99.9% cases even if it were a signed sized type. (compare ssize_t)
  • The fact that std::vector and friends chose size_t, an unsigned type, for the size and indexing is considered by some to be a design flaw. I concur. (Seriously, take 5 minutes and watch the lightning talk CppCon 2016: Jon Kalb “unsigned: A Guideline for Better Code".)
  • When you design an C++ API today, you're in a tight place: Use size_t to be consistent with the Standard Library, or use (a signed) intptr_t or ssize_t for easy and less bug prone indexing calculations.
  • Don't use int32 or int64 - use intptr_t if you want to go signed, and want machine word size, or use ssize_t.

To directly answer the question, it is not entirely an "historical artefact", as the theoretical issue of needing to address more than half the ("indexing", or) address space must be, aehm, addressed somehow in a low level language like C++.

In hindsight, I, personally, think, it is a design flaw that the Standard Library uses unsigned size_t all over the place even where it does not represent a raw memory size, but a capacity of typed data, like for the collections:

  • given C++s integer promotion rules ->
  • unsigned types just don't make good candidates for "semantic" types for something like a size that is semantically unsigned.

I'll repeat Jon's advice here:

  • Select types for the operations they support (not the range of values). (*1)
  • Don't use unsigned types in you API. This hides bugs with no upside benefit.
  • Don't use "unsigned" for quantities.(*2)

(*1) i.e. unsigned == bitmask, never do math on it (here hits the first exception - you may need a counter that wraps - this must be an unsigned type.)

(*2) quantities meaning something you count and/or do math on.

2
  • What do you mean with "full avilable flat memory"? Also, sure you don't want ssize_t, defined as the signed pendant to size_t instead of intptr_t, which can store any (non-member-)pointer and might thus be bigger? Commented Dec 5, 2018 at 1:30
  • @Deduplicator - Well I guess I may have gotten the size_t definition slightly messed up. See size_t vs. intptr and en.cppreference.com/w/cpp/types/size_t Learned something new today. :-) I think the rest of the arguments stand, I'll see if I can fix the types used.
    – Martin Ba
    Commented Dec 5, 2018 at 9:35
0

I'll just add that for performance reasons I normally use size_t, to ensure that miscalculations cause an underflow which means both range checks (below zero and above size()) can be reduced to one:

using signed int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

using unsigned int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}
3
  • 1
    You really want to explain that one more thoroughly.
    – Martin Ba
    Commented Dec 20, 2016 at 12:45
  • To make the answer more useful, perhaps you can describe how the integer array bounds or offset comparison (signed and unsigned) looks like in the machine code from various compiler vendors. There are many online C++ compilers and disassembly sites that can show the corresponding compiled machine code for the given C++ code and compiler flags.
    – rwong
    Commented Dec 20, 2016 at 13:04
  • I tried to explain this some more.
    – asger
    Commented Dec 20, 2016 at 13:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.