31

I'm quite new to C++ but I find this behaviour of auto weird:

class A{};

int main() {
    A a;
    auto x = -(sizeof(a));
    cout << x << endl;
    return 0;
}

Variable x is unsigned in this case although I used the unary minus operator at the initialiation of the variable. How come that only the return type of sizeof (std::size_t) is considered but not the fact that the stored number will be negative because of the used operator?

I'm aware of size_t being an unsigned int.

I've tried this with GCC 8.1.0 and C++17.

Boann
  • 48,794
  • 16
  • 117
  • 146
CodeShark
  • 1,709
  • 2
  • 14
  • 22
  • 9
    why would you expect something different? The same thing happens when you do `cout << -(sizeof(a)) << endl;`. This has zero to do with auto. – PeterT Jul 25 '18 at 08:22
  • My thought was that `auto` does not only respect the return type but do some kind context check like checking for unary minus operator or whatever happens in whole initialization process. – CodeShark Jul 25 '18 at 08:24
  • 5
    Because C designers wanted `-` to be valid on unsigned values. Personally I would have rendered it invalid. – Johannes Schaub - litb Jul 25 '18 at 08:26
  • 10
    That might be a misunderstanding about `auto` it does not add anything new to the C++ language, it uses the very same mechanism that was already present in the language for template argument type deduction – PeterT Jul 25 '18 at 08:26
  • I added tags but not one related to auto, type deduction, template, as it is an arithmetic issue not a template issue. But auto is defined in term of templates. – curiousguy Jul 25 '18 at 08:46
  • @JohannesSchaub-litb Which operators would you allow for unsigned? – curiousguy Jul 25 '18 at 08:47
  • 1
    @curiousguy all except unary minus. By saying `0u - x`, you can get the same effect as `-x`. Perhaps I would even forbid mixing signedness/unsignedness in operator expressions. IMO it could be useful to still allow `-1u`.. so I could add a rule that unary minus on unsigned is only allowed on unsigned literals, which kind-of is similar to forbidding mixing of signedness in operator expressions. – Johannes Schaub - litb Jul 25 '18 at 08:50
  • @JohannesSchaub-litb So subtraction of unsigned numbers, which is modulo, is OK, but not negation? – curiousguy Jul 25 '18 at 08:53
  • 2
    @curiousguy yes because wrap-around is just a special case for subtraction. Only if right is > than left, you have wrap around. For negation on the other hand, wrap around is the majority of cases, with just `0` being the exception. – Johannes Schaub - litb Jul 25 '18 at 08:59
  • 1
    BTW, no need for all those unnecessary parens - `- sizeof a` demonstrates the behaviour just as effectively. – Toby Speight Jul 25 '18 at 13:46

4 Answers4

28

The actual issue here is that use of unary minus operator, just like the rest of built-in arithmetic operators, is a subject to integral promotions. So surprisingly the result of applying unary minus to size_t will be still size_t and there is no need to blame auto.

Counter-example. In this case due to integral promotions type of x will be int so output will be -1:

unsigned short a{1};
auto x{-a};
cout << x << endl;
user7860670
  • 35,849
  • 4
  • 58
  • 84
  • Don't you mean **un**-surprisingly? – acraig5075 Jul 25 '18 at 08:27
  • 3
    @acraig5075 No, I mean surprisingly. It is rather confusing why unary minus is allowed to be applied to the unsigned type on the first place. I mean unary minus is supposed to change the sign of the value, but unsigned values can not have the sign changed. – user7860670 Jul 25 '18 at 08:30
  • 2
    VTT binary minus is applicable to unsigned values so why shouldn't unary minus? – Caleth Jul 25 '18 at 08:34
  • 9
    What does integral promotion have to do with it? Nothing is promoted here as far as I can see; `size_t` in, `size_t` out...?!? – DevSolar Jul 25 '18 at 08:35
  • 1
    @DevSolar Yes, output type remains `size_t` according to the rules of integral promotions. – user7860670 Jul 25 '18 at 08:37
  • @Caleth Binary minus is supposed to decrement the value so it makes sense in general. While unary minus is supposed to change the sign, which does not make much sense since the sign of the unsigned type can not be changed. – user7860670 Jul 25 '18 at 08:38
  • @Caleth Technically unary minus applied to unsigned type *"is computed by subtracting its value from 2 n, where n is the number of bits in the promoted operand"* which is not exactly the same as "binary minus partially applied to an appropriate zero " – user7860670 Jul 25 '18 at 08:49
  • It's equivalent under arithmetic mod 2^n – Caleth Jul 25 '18 at 08:53
  • 2
    @VTT You can either think of an unsigned integer as a positive integer or an integer modulo 2^n. Negation of an unsigned being unsigned makes sense only in term of modulo arithmetic (Z/(2^n)Z). But saying that a number of bytes is an integer modulo is absurd, it's a positive number. – curiousguy Jul 25 '18 at 08:55
  • @DevSolar is right, that's why I downvoted it. The answer says that the issue comes from integral promotion. But even if you remove integral promotion from the spec, you still have "size_t in, size_t out". – Johannes Schaub - litb Jul 25 '18 at 08:56
  • @JohannesSchaub-litb If you remove integral promotions from the spec (that is replace them with different set of rules) type of result of applying unary minus to the unsigned type could be anything, including negative number as OP expects or such an operation being prohibited. The primary point of the answer is that `auto` works correctly. – user7860670 Jul 25 '18 at 09:10
  • No it could not be anything. Or are you saying because my litb-promotions were not adopted into the spec, the type of "x" can actually be anything even today? By "removing the rules that you make responsible", I don't mean "replacing by a random other rule". – Johannes Schaub - litb Jul 25 '18 at 09:43
  • It is true that integral promotions apply formally. But they are not responsible for this result, because they keep the result type equal to the source type. It asks for misinterpretation by readers to pick a random screw that even has no function at all in the problem and blame it. – Johannes Schaub - litb Jul 25 '18 at 09:50
  • @JohannesSchaub-litb This is nonsense. Integral promotions are directly responsible for the type of the result. And it is the same thing with the most of the other built-in arithmetic operators, the type of the result is determined by the usual arithmetic conversions / integral promotions rather than by operator itself. "removing rules" can only mean making subject either implementation-defined or completely undefined anything else would be replacing rules. – user7860670 Jul 25 '18 at 10:26
  • 1
    @DevSolar Integral promotions are responsible for the type of the result, rather than minus operator, which yields result of the same type as (promoted) input. In this case `size_t` remains `size_t` but situation can be different, for example in `unsigned short x{1}; auto a{-x};` type of `a` will be `int` all of it sudden. – user7860670 Jul 25 '18 at 10:28
  • @VTT: I understand that. It's just that there is no promotion happening in the OP's example, so I thought bringing it up in the answer to be a bit confusing. – DevSolar Jul 25 '18 at 10:30
  • 1
    @DevSolar It actually does happen, but yields the same `size_t`. – user7860670 Jul 25 '18 at 10:31
  • @ilkkachu I reverted. "An unsigned value does not promote to a signed value" is actually wrong as I demonstrated at counter-example. – user7860670 Jul 25 '18 at 10:41
  • And BTW the fact that there is promotion even to a wider type contradicts the whole unsigned is modular over 2^n. A modular value should only be implicitly converted to a smaller module. Converting to a larger module should be an explicit operation. And converting to a non modular type is an even more serious contradiction of "unsigned is modular". The whole construct makes no sense conceptually. – curiousguy Jul 25 '18 at 11:49
  • "so output will be -1" the output of the program depends on the relative sizes of the built-in types. If int is larger than short then the result will indeed be -1. OTOH if int is the same size as short then the result will be the maximum value of unsigned short. – plugwash Jul 25 '18 at 12:46
  • 1
    @plugwash Illustrating the craziness of promotion rules – curiousguy Jul 25 '18 at 14:31
  • I rechecked the wording of "[conv.prom]". In fact, the type `unsigned` is not even eligible to be promoted to anything, not even to itself. The integral promotion standard conversion is only applicable to certain other smaller types. – Johannes Schaub - litb Jul 25 '18 at 14:54
  • On second sight, it is even more complicated, because the integer conversion rank is implementation defined, however "Note: It is recommended that implementations choose types for ptrdiff_­t and size_­t whose integer conversion ranks are no greater than that of signed long int unless a larger size is necessary to contain all the possible values." – Johannes Schaub - litb Jul 25 '18 at 15:01
  • 3
    @curiousguy: The fundamental problem is that unsigned types are used for two disjoint purposes: to process modular arithmetic, or to hold natural numbers whose upper limit exceeds that of the corresponding signed type by a factor of less than two. Unfortunately, rather than allowing programmers to specify which is needed, C simply assumes that "small" unsigned types should have the latter behavior an "full-sized" ones should have the former. – supercat Jul 25 '18 at 16:06
21

Your expression -(sizeof(a)) applies the unary - operator to a value of unsigned type. The unary - operator does not turn an unsigned integral value into a signed one; it rather defines which unsigned value will be the result of such an operation as follows (cf. unary arithmetic operators at cppreference.com):

The builtin unary minus operator calculates the negative of its promoted operand. For unsigned a, the value of -a is 2^b -a, where b is the number of bits after promotion.

Hence, even if it may be surprising, auto works correctly, as the result of applying unary - operator to an unsigned value is still an unsigned value.

Stephan Lechner
  • 34,891
  • 4
  • 35
  • 58
  • 4
    "_operator does not turn an unsigned integral value into a signed one_" Which entirely makes senses if you use unsigned for modulo arithmetic and makes no sense what so ever if you use unsigned for natural numbers – curiousguy Jul 25 '18 at 08:48
  • You might like to mention that `size_t` like all integral types is subject to integral promotion, so iff it is smaller than `int`, it will be promoted to `int` before unary minus gets it. But I don't know any such perverse implementation. – Deduplicator Jul 25 '18 at 16:59
4

The result of (unary) - applied to an unsigned value is unsigned, and sizeof returns an unsigned value.

The operand of the unary - operator shall have arithmetic or unscoped enumeration type and the result is the negation of its operand. Integral promotion is performed on integral or enumeration operands. The negative of an unsigned quantity is computed by subtracting its value from 2^n, where n is the number of bits in the promoted operand. The type of the result is the type of the promoted operand.

[expr.unary.op]

The result of sizeof and sizeof... is a constant of type std​::​size_­t

[expr.sizeof]

To avoid implementation defined behaviour, you have to convert to int before applying the -

If the destination type is signed, the value is unchanged if it can be represented in the destination type; otherwise, the value is implementation-defined.

[conv.integral]

class A{};

int main() {
    A a;
    auto x = -(int{sizeof(a)});
    cout << x << endl;
    return 0;
}
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • Thanks for showing the `auto x = 0-(sizeof(a));` way. – CodeShark Jul 25 '18 at 08:33
  • If `size_t` has a range larger than `int` (almost always the case), then given `size_t x,y=...;`, then `x=-y;` will set `x` to the one possible `size_t` value that would make `x+y` yield 0. – supercat Jul 25 '18 at 16:11
1

If we take a look at: https://en.cppreference.com/w/cpp/language/sizeof, the result is of type size_t which is unsigned. You explicity need to declare it as an signed int to allow negative values.

Instead of auto you can write int which allows negative values.

gi097
  • 7,313
  • 3
  • 27
  • 49
  • So do I have to cast it with explicitely saying it's a `signed int`? Is `auto` always only using the return type without checking if it makes sense? If so, I see an issue with casting here. – CodeShark Jul 25 '18 at 08:21
  • Exactly, you need to declare `c++` that you are going to store a negative value. `auto` does not know about this since the value was `size_t`. Casting will not give you an issue since you tell `c++` it will be a `signed int`. – gi097 Jul 25 '18 at 08:21
  • @CodeShark What doesn't "make sense" here? – curiousguy Jul 25 '18 at 08:49