[C++20] Three-Way Comparison / Consteval 연구

MIN·2025년 6월 7일
0

CPP20

목록 보기
5/8

Three-Way Comparison

3방향 비교 연산자(three-way comparison)은 주어진 표현식의 평가 결과가 비교 대상이 되는 값과 같은지 아니면 그보다 크거나 작은지 알려주는 연산자이다.

3방향 비교 연산자(three-way comparison)는 두 값을 비교해서 "작다", "같다", "크다" 중 하나의 결과를 반환하는 연산자다.
보통 ==, <, >, <= 같은 연산자를 따로 정의하던 방식에서 벗어나, a <=> b 하나만으로 모든 비교가 가능해진다.

C++20부터 도입된 이 연산자는 연산 결과로 true나 false를 주는 것이 아니라, 세 가지 방향 중 어디에 해당하는지 나타내는 값을 리턴한다.
그래서 별명도 우주선 연산자(spaceship operator)다. 생긴 게 <=> 이렇게 생겨서 그렇다.

다음은 간단한 구조체 예제다.

struct MyInt
{
	MyInt(int value) : _value(value) { }

	auto operator<=>(const MyInt& rhs) const = default;

	int _value;
};

int main()
{
	MyInt a{ 1 };
	MyInt b{ 2 };

	bool test = a < b;
	bool test2 = a > b;
	bool test3 = a <= b;
}

operator<=>를 = default로 정의하면, 해당 타입의 모든 멤버를 자동으로 비교해준다.
기존 방식이라면 <, ==, !=, > 등을 모두 오버로딩해야 했지만, 이제는 단 하나의 연산자만으로 끝낼 수 있다.

이 연산자를 사용해서 나오는 결과는 피연산자의 타입에 따라서 달라진다.

정수처럼 비교가 명확하게 가능한 타입은 strong_ordering이 리턴된다.
다음 세 가지 값 중 하나다.

  • strong_ordering::less : 첫 번째 피연산자가 두 번째 피연산자보다 작다.
  • strong_ordering::greater : 첫 번째 피연산자가 두 번째 피연산자보다 크다.
  • strong_ordering::equal : 두 피연산자가 같다.
int main()
{
	int a1 = 100;
	int b1 = 200;
	
    // 정수이기 때문에 strong_ordering 리턴
	auto ret = (a1 <=> b1);

	if (ret < 0)
		cout << "a < b, less" << endl;		-> 결과 출력
	else if (ret == 0)
		cout << "a == b, equal" << endl;
	else if (ret > 0)
		cout << "a > b greater" << endl;
}

부동소수점처럼 NaN 같은 비교 불가능한 값이 존재할 수 있는 타입은 partial_ordering을 사용한다.

  • partial_ordering::less : 첫 번째 피연산자가 두 번째 피연산자보다 작다.
  • partial_ordering::greater : 첫 번째 피연산자가 두 번째 피연산자보다 크다.
  • partial_ordering::equivalent : 두 피연산자가 같다.
  • partial_ordering::unordered : 두 피연산자 중 하나는 숫자가 아니다.

사실 weak_ordering이라는 것도 있긴 하지만 실무에서 자주 쓰이진 않기 때문에 여기서는 생략하겠다.
중요한 건, 객체 단위의 비교가 많을 때 이 연산자를 사용하면 성능이나 코드 관리 측면에서 이점이 많다는 점이다.
기존에는 여러 번의 비교 연산자 호출이 필요했지만, 3방향 비교 연산자 하나만 정의해두면 그걸 기준으로 모든 비교를 유도할 수 있다.

Consteval

const는 C++에서 가장 많이 쓰이는 한정자 중 하나다.
멤버 변수나 함수를 상수로 만들어서 값을 변경할 수 없도록 만든다.

이후 C++11에 들어오면서 constexpr이 등장했다. 이 역시 상수성을 부여하지만,
차이점은 constexpr은 컴파일 타임에 평가되는 값이나 함수를 선언할 수 있게 해준다는 점이다.

그러기 위해서는 먼저 컴파일 타임과 런타임 개념부터 확실히 알고 가야 한다.

컴파일 타임 vs 런타임

컴파일 타임은 프로그램의 소스 코드가 기계어로 변환되는 시점을 의미한다.
주로 이 작업 시간에 문법 검사, 타입 검사, 오류 감지 등의 작업이 수행된다.
컴파일 타임을 통해서 실행 중 발생할 수 있는 오류를 최소화할 수 있다.

런타임은 프로그램이 실제로 진행되고 동작하는 시간이다.
사용자가 입력한 데이터를 처리하고, 결과를 출력하며, 다양한 시스템 자원을 활용하여 작업을 수행하는 시간이다.

이 둘의 차이는 값을 미리 알 수 있느냐, 실행해봐야 아느냐의 차이다.
컴파일 타임에는 값을 미리 알아야 한다.
하지만 런타임에는 실행 중에야 값이 정해진다.
그래서 런타임 값을 컴파일 타임에 사용하면 컴파일 오류가 난다.

constexpr vs consteval

먼저 constexpr을 보자.
컴파일 타임에 평가되도록 시도하는 한정자다. 하지만 항상 그렇게 되지는 않는다.

constexpr int SqrRunOrCompileTime(int n)
{
	return n * n;
}

int main()
{
	int a = 10;
	
	// 런타임
	int val2 = SqrRunOrCompileTime(a);				-> 작동
    
    // 컴파일 타임
    constexpr int val2_2 = SqrRunOrCompileTime(a);	-> 오류
}

보면 알 수 있듯이, a는 런타임에 값이 결정되기 때문에 constexpr로 초기화할 수 없다.
이처럼 constexpr은 유연하지만 컴파일 타임 평가를 강제할 수는 없다는 단점이 있다.

이 문제를 해결하기 위해 C++20에 새로 등장한 키워드가 consteval이다.

consteval은 함수가 무조건 컴파일 타임에 평가되도록 강제한다.
컴파일 타임이 아니면 아예 사용할 수 없다.

consteval int SqrCompileTime(int n)
{
	return n * n;
}

int main()
{
	int a = 10;

	// 컴파일
    int val3 = SqrCompileTime(10);	-> 작동
	int val3 = SqrCompileTime(a);	-> 오류
}

consteval 함수는 말 그대로 컴파일 타임에서만 살아 있는 함수다.
런타임 값을 넣으려고 하면 무조건 컴파일 에러가 난다.

그렇다고 매번 숫자를 직접 넣는 건 불편할 수도 있다.
이럴 때는 enum을 활용하면 좋다.
enum 상수는 컴파일 타임에 평가되기 때문에 consteval 함수에 사용할 수 있다.

int main()
{
	enum
	{
		VAL = 1,
	}
	
	// 컴파일
    int val3 = SqrCompileTime(VAL);	-> 작동
}

이제 consteval을 이해하면, 진짜로 컴파일 타임에만 작동하는 함수를 작성할 수 있게 된다.
constexpr보다 더 강한 제약을 주고 싶을 때 유용한 무기다.
실제 사용에서는 컴파일 타임 최적화나 템플릿 메타프로그래밍 같은 상황에서 빛을 발하게 된다.

마무리

이번에 살펴본 3방향 비교 연산자와 consteval은 각각 분명한 장점이 있다.
3방향 비교 연산자는 비교 연산자 오버로딩을 간소화해주고,
consteval은 컴파일 타임에 확실하게 평가해야 할 때 강력한 제약을 걸 수 있다.

하지만 필자 입장에서는 아직까지 이 둘을 실제로 사용할 일은 거의 없을 것 같다.
솔직히 말해서, 굳이 이 정도 기능을 쓰겠다고 C++20으로 올릴 필요가 있을까? 라는 생각이 들 정도다.
C++은 예전부터 이런 기능이 없어도 충분히 잘 굴러갔다.
오히려 버전을 올리면서 생기는 호환성 문제가 더 골치 아픈 부분일 수 있다.

결국 중요한 건, 이 기능들을 억지로 사용하는 게 아니라
문법을 이해하고, 남이 쓴 코드를 읽을 수 있을 정도의 감각만 익혀두는 것이다.
그 정도면 충분하다고 생각한다.
그리고 정말 필요해졌을 때 꺼내 쓰면 된다.
지금은 그 정도의 거리감으로 이 개념들을 받아들이는 것이 가장 현실적인 접근일 것 같다.

참고

Inflearn [Rookiss][C++20 훑어보기]
전문가를 위한 C++(개정5판) P87~P88, P129~P131
C++ 공식문서

profile
게임개발자(진)

0개의 댓글