Why does 'auto' not respect the unary minus operator?
Asked Answered
D

4

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.

Despondent answered 25/7, 2018 at 8:14 Comment(10)
why would you expect something different? The same thing happens when you do cout << -(sizeof(a)) << endl;. This has zero to do with auto.Rockribbed
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.Despondent
Because C designers wanted - to be valid on unsigned values. Personally I would have rendered it invalid.Firecure
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 deductionRockribbed
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.Chivalry
@JohannesSchaub-litb Which operators would you allow for unsigned?Chivalry
@Chivalry 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.Firecure
@JohannesSchaub-litb So subtraction of unsigned numbers, which is modulo, is OK, but not negation?Chivalry
@Chivalry 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.Firecure
BTW, no need for all those unnecessary parens - - sizeof a demonstrates the behaviour just as effectively.Driblet
G
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;
Geocentric answered 25/7, 2018 at 8:25 Comment(24)
Don't you mean un-surprisingly?Subdivide
@Subdivide 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.Geocentric
VTT binary minus is applicable to unsigned values so why shouldn't unary minus?Lytta
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...?!?Heins
@Heins Yes, output type remains size_t according to the rules of integral promotions.Geocentric
@Lytta 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.Geocentric
@Lytta 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 "Geocentric
It's equivalent under arithmetic mod 2^nLytta
@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.Chivalry
@Heins 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".Firecure
@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.Geocentric
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".Firecure
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.Firecure
@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.Geocentric
@Heins 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.Geocentric
@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.Heins
@Heins It actually does happen, but yields the same size_t.Geocentric
@ilkkachu I reverted. "An unsigned value does not promote to a signed value" is actually wrong as I demonstrated at counter-example.Geocentric
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.Chivalry
"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.Zosima
@Zosima Illustrating the craziness of promotion rulesChivalry
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.Firecure
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."Firecure
@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.Spoken
V
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.

Veta answered 25/7, 2018 at 8:34 Comment(2)
"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 numbersChivalry
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.Whittle
L
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;
}
Lytta answered 25/7, 2018 at 8:31 Comment(2)
Thanks for showing the auto x = 0-(sizeof(a)); way.Despondent
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.Spoken
V
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.

Verile answered 25/7, 2018 at 8:19 Comment(3)
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.Despondent
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.Verile
@Despondent What doesn't "make sense" here?Chivalry

© 2022 - 2024 — McMap. All rights reserved.