Why does the C++ modulo operator return 0 for -1 % str.size()?
Asked Answered
D

3

3

I'm confused why the following code produces this output:

#include <iostream>
#include <string>

using namespace std;

int main()
{
    int i = -1;
    string s = "abc";
    int j = s.size();
    int x = 1 % 3;
    int y = i % j; 
    int z = i % s.size(); 
    cout << s.size() << endl; // 3
    cout << x << endl;        // 1
    cout << y << endl;        // -1
    cout << z << endl;        // 0
}

Why is z = 0? Does it have to do with casting?

Derosa answered 19/4, 2020 at 3:52 Comment(6)
Without testing, I bet this is related to signed-ness.Standifer
Pro tip: see what static_cast<size_t>(-1) is. Then calculate its remainder when divided by 3. Spoiler alert: it's 0.Huckaby
It's not a modulus operator. It is a remainder operator.Stylo
Yes is casting, s.size() is unsigned, -1 transformed to unsigned is 2^32-1, which is divisible by 3Transposal
Exactly what @EduardoPascualAseff said. Whats happening here is datatype overflow. -1 does not exist as an unsigned integer. So it overflows to (max unsigned int - 1).Tindal
@EduardoPascualAseff -- it's not casting, it's converting. There are no casts in the code in the question. A cast is something you write in your source code to tell the compiler to do a conversion.Procopius
A
2

So, stripping down your code to a minimal example, you're asking why this prints 0:

#include <iostream>
#include <string>

int main()
{
    int a = -1;
    std::string::size_type b = 3; 
    int c = a % b;
    std::cout << c << '\n';
}

The primary operation in question here is this:

a % b

Per the standard,

5.6 Multiplicative operators [expr.mul]

  1. The operands of * and / shall have arithmetic or unscoped enumeration type; the operands of % shall have integral or unscoped enumeration type. The usual arithmetic conversions are performed on the operands and determine the type of the result.

So.. what about those "usual arithmetic conversions"? This is to mate the types of the two operands to a common type prior to performing the actual operation. The following are considered in order :

  • If both operands are integers, integer promotion is first performed on both operands. If after integer promotion the operands still have different types, conversion continues as follows:
    • If one operand has an unsigned type T whose conversion rank is at least as high as that of the other operand’s type, then the other operand is converted to type T.
    • Otherwise, one operand has a signed type T whose conversion rank is higher than that of the other operand’s type. The other operand is converted to type T only if type T is capable of representing all values of its previous type.
    • Otherwise, both operands are converted to the unsigned type that corresponds to the signed type T.

That's a lot of legalize for what effectively says this:

  • You have two operands, a signed int and a std::string::size_type
  • The rank of std::string::size_type is greater than that of signed int
  • Therefore, the signed int operand is converted to type std::string:size_type prior to the operation being requested.

So all that is left is the conversion, to wit, there is one more piece of legalize:

4.7 Integral conversions [conv.integral]

  1. 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]

That means, on a 32-bit std::string::size_type platform, you're going to get 232-1 as the converted value from int (-1).

Which means...

4294967295 % 3

Which is... zero. If std::string::size_type is 64-bits, then everything above stays the same, save for the final calculation, which would be:

18446744073709551615 % 3
Allowance answered 19/4, 2020 at 5:14 Comment(0)
P
3

What really happens here:

int z = i % s.size();

is i is converted to size_t because the other side s.size() is size_t. And unlike int, size_t is unsigned; That is to say the value is interpreted as a positive number.

Check the output of this line:

std::cout << (size_t)-1 << std::endl;

to see what -1 has become.

Proceleusmatic answered 19/4, 2020 at 4:13 Comment(3)
Sure, thanks. I will edit my answer. The point is: the sign change is causing the behavior here.Proceleusmatic
@JaMiT And size_t is unsigned. I don't see anything wrong here except that 'unsigned' should just be 'unsigned'Stylo
@Stylo That would be a correction. There is also the cast in the code, though.Quintal
A
2

So, stripping down your code to a minimal example, you're asking why this prints 0:

#include <iostream>
#include <string>

int main()
{
    int a = -1;
    std::string::size_type b = 3; 
    int c = a % b;
    std::cout << c << '\n';
}

The primary operation in question here is this:

a % b

Per the standard,

5.6 Multiplicative operators [expr.mul]

  1. The operands of * and / shall have arithmetic or unscoped enumeration type; the operands of % shall have integral or unscoped enumeration type. The usual arithmetic conversions are performed on the operands and determine the type of the result.

So.. what about those "usual arithmetic conversions"? This is to mate the types of the two operands to a common type prior to performing the actual operation. The following are considered in order :

  • If both operands are integers, integer promotion is first performed on both operands. If after integer promotion the operands still have different types, conversion continues as follows:
    • If one operand has an unsigned type T whose conversion rank is at least as high as that of the other operand’s type, then the other operand is converted to type T.
    • Otherwise, one operand has a signed type T whose conversion rank is higher than that of the other operand’s type. The other operand is converted to type T only if type T is capable of representing all values of its previous type.
    • Otherwise, both operands are converted to the unsigned type that corresponds to the signed type T.

That's a lot of legalize for what effectively says this:

  • You have two operands, a signed int and a std::string::size_type
  • The rank of std::string::size_type is greater than that of signed int
  • Therefore, the signed int operand is converted to type std::string:size_type prior to the operation being requested.

So all that is left is the conversion, to wit, there is one more piece of legalize:

4.7 Integral conversions [conv.integral]

  1. 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]

That means, on a 32-bit std::string::size_type platform, you're going to get 232-1 as the converted value from int (-1).

Which means...

4294967295 % 3

Which is... zero. If std::string::size_type is 64-bits, then everything above stays the same, save for the final calculation, which would be:

18446744073709551615 % 3
Allowance answered 19/4, 2020 at 5:14 Comment(0)
C
1

@GhaziMajdoub's answer is correct, but - why don't you let the compiler tell you what's happening?

Let's use the Flags to enable thorough and verbose g++ warnings ...

$ g++ -pedantic -Wall -Wextra -Wcast-align -Wcast-qual -Wctor-dtor-privacy \ 
-Wdisabled-optimization -Wformat=2 -Winit-self -Wlogical-op -Wmissing-declarations \
-Wmissing-include-dirs -Wnoexcept -Wold-style-cast -Woverloaded-virtual \
-Wredundant-decls -Wshadow -Wsign-conversion -Wsign-promo -Wstrict-null-sentinel \
-Wstrict-overflow=5 -Wswitch-default -Wundef -Werror -Wno-unused -o a a.cpp
a.cpp: In function ‘int main()’:
a.cpp:12:13: error: conversion to ‘std::__cxx11::basic_string<char>::size_type’ {aka
‘long unsigned int’} from ‘int’ may change the sign of the result
[-Werror=sign-conversion]
   12 |     int z = i % s.size();
      |             ^
cc1plus: all warnings being treated as errors

a.cpp: In function ‘int main()’:
a.cpp:12:13: warning: conversion to ‘std::__cxx11::basic_string<char>::size_type’ 
{aka ‘long unsigned int’} from ‘int’ may change the sign of the result [-Wsign-
conversion]
   12 |     int z = i % s.size();
      |             ^

and there you have it: i is converted to long unsigned int, so it's no longer -1.

Chirm answered 19/4, 2020 at 5:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.