HOWTO · C++
C++에서 모듈로 연산자 사용
이 기사에서는 C++에서 모듈로 연산자를 사용하는 방법을 소개합니다.
이 페이지의 내용
C++ % 연산자는 정수 나눗셈의 나머지를 반환합니다. 예를 들어 10 % 3의 결과는 1입니다. 부호 있는 정수를 포함한 정수형에서 사용할 수 있지만, 부동 소수점 피연산자는 허용되지 않습니다. 나누는 값은 0이 아니어야 합니다. 부호 있는 값에 필요한 처리를 고려하면, 나머지는 홀짝 확인, 순환 위치 계산, 범위가 제한된 버킷 인덱스 등에 사용할 수 있습니다.
C++ % 연산자가 반환하는 값
정수 a와 b에 대해 a % b는 a / b를 계산한 뒤 남는 나머지입니다. C++의 정수 나눗셈은 0을 향해 소수 부분을 버리므로 10 / 3은 3이고 10 % 3은 1입니다. 두 피연산자는 모두 정수형이어야 합니다. 정수형이 아닌 피연산자에 %를 사용하면 식이 올바른 형식이 아니게 됩니다(ill-formed). 또한 나누는 값은 0이 아니어야 합니다. 0으로 나누면 정의되지 않은 동작이 발생합니다.
다음 간단한 프로그램은 양수의 일반적인 계산을 보여 줍니다.
#include <iostream>
int main() {
const int dividend = 17;
const int divisor = 5;
std::cout << dividend % divisor << '\n';
}
2
부동 소수점 값에는 % 연산자를 사용할 수 없습니다. 부동 소수점 나머지가 필요하면 <cmath>의 std::fmod를 사용하세요.
부호 있는 정수에서 %가 작동하는 방식
부호 있는 피연산자를 사용해도 정수 나눗셈은 0을 향해 소수 부분을 버리며, 0이 아닌 나머지의 부호는 피제수의 부호와 같습니다. 따라서 -10 % 3은 -1이고 10 % -3은 1입니다. 두 경우 모두 몫에 제수를 곱한 값에 나머지를 더하면 피제수가 됩니다. 이 동작은 컴파일러의 선택에 맡겨진 것이 아니라 C++에서 정한 규칙입니다.
음수 나머지는 음수로 남으므로 %만으로는 항상 음이 아닌 인덱스를 얻을 수 없습니다. 결과를 정규화하려면 양수 모듈러스 m을 사용하고, value % m을 나머지 변수에 저장한 다음 그 나머지가 음수일 때만 m을 더하세요. 나머지는 -m보다 크므로, 중간 합에서 오버플로가 발생하지 않으면서 결과가 [0, m) 범위에 들어갑니다. 몫을 해당 정수형으로 표현할 수 있어야 한다는 조건도 있습니다. 예를 들어 부호 있는 정수형의 최솟값을 % -1로 계산하는 경우처럼, 대응하는 정수 나눗셈의 몫을 표현할 수 없으면 %는 정의되지 않은 동작을 일으킵니다. 위의 나머지 항등식은 이 정의되지 않은 경우에는 적용되지 않습니다.
%로 홀짝을 확인하고 값을 순환시키기
정수는 value % 2 == 0이면 짝수이고, 그렇지 않으면 홀수입니다. 2의 배수는 나머지가 0이므로 음수에도 이 방법을 사용할 수 있습니다.
순환 인덱싱을 할 때는 컬렉션 크기가 양수이고 위치가 음이 아닌 값이어야 합니다.
#include <iostream>
int main() {
const int values[] = {10, 20, 30};
const int count = static_cast<int>(sizeof(values) / sizeof(values[0]));
for (int position = 0; position < 5; ++position) {
const int index = position % count;
std::cout << values[index] << '\n';
}
}
10
20
30
10
20
배열에는 원소가 세 개 있으므로 각 인덱스는 0부터 2까지입니다. 반복문이 진행되면 위치가 같은 순서로 다시 돌아옵니다. 이 방법을 쓰려면 컬렉션에 원소가 하나 이상 있어야 합니다. 위치가 음수일 수 있다면 인덱스로 사용하기 전에 나머지를 정규화하세요.
해싱과 난수 범위에 나머지 사용하기
해시 테이블에서는 나머지를 사용해 음이 아닌 키를 tableSize개의 버킷 중 하나에 대응시킬 수 있습니다. 계산식은 key % tableSize입니다. 이때 tableSize > 0이어야 합니다. 키가 음수일 수 있다면 인덱스로 변환하기 전에 앞에서 설명한 방법으로 결과를 정규화하세요. 실제 해시 테이블에는 해시 함수와 충돌 처리 전략도 필요합니다. 나머지는 버킷 범위 안에서 버킷을 선택하는 역할만 합니다.
난수를 만들 때 std::rand() % bound의 결과가 균등하게 분포한다고 가정하지 마세요. 생성기가 낼 수 있는 값의 개수가 bound의 배수가 아니면 일부 결과가 더 자주 나올 수 있습니다. 포함 범위가 있는 정수 난수가 필요하다면 난수 엔진과 함께 std::uniform_int_distribution을 사용하는 편이 좋습니다.
#include <iostream>
#include <random>
int main() {
std::mt19937 engine(12345);
std::uniform_int_distribution<int> range(1, 6);
std::cout << range(engine) << '\n';
}
6
분포의 매개변수는 포함되는 하한과 상한을 정합니다. 이 시드와 엔진, 그리고 테스트한 표준 라이브러리 구현의 std::uniform_int_distribution에서는 예제가 6을 출력합니다. C++은 표준 라이브러리 구현마다 엔진 출력을 분포에 대응시키는 방식이 같아야 한다고 요구하지 않으므로, 정확한 출력과 수열은 이식 가능하지 않습니다. 엔진과 분포 구현이 모두 같을 때만 결과가 반복됩니다. 서로 다른 수열이 필요하면 애플리케이션에 적합한 시드 출처를 사용하세요.
함수 객체로 사용하는 std::modulus
<functional>에 있는 std::modulus는 두 값에 %를 적용하는 함수 객체입니다. 연산자 식 대신 호출 가능한 객체를 요구하는 알고리즘에서 유용합니다.
#include <functional>
#include <iostream>
int main() {
const std::modulus<int> remainder;
std::cout << remainder(17, 5) << '\n';
}
2
피연산자에는 %와 동일한 정수형 및 0이 아닌 제수 규칙이 적용됩니다. 일반적인 식에서는 a % b라고 쓰는 편이 대체로 가장 명확합니다.