콘텐츠로 이동

연산자 오버로딩 (Operator Overloading)

연산자를 신중하게 오버로드하세요. 사용자 정의 리터럴을 사용하지 마세요.

정의:

C++는 매개변수 중 하나가 사용자 정의 타입이기만 하면, 사용자 코드가 operator 키워드로 내장 연산자의 오버로드된 버전을 선언하는 것을 허용합니다. 또한 operator 키워드를 쓰면 operator""로 새로운 종류의 리터럴을 정의하거나, operator bool() 같은 타입 변환 함수를 정의할 수도 있습니다.

장점:

연산자 오버로딩은 사용자 정의 타입이 내장 타입과 똑같이 동작하게 해서 코드를 더 간결하고 직관적으로 만들 수 있습니다. 오버로드된 연산자는 특정 연산에 대한 관용적인 이름이며(예: ==, <, =, <<), 이 관례를 따르면 사용자 정의 타입이 더 읽기 좋아지고 그런 이름을 기대하는 라이브러리와 상호 운용할 수 있습니다.

사용자 정의 리터럴은 사용자 정의 타입의 객체를 만드는 매우 간결한 표기법입니다.

단점:

  • 정확하고 일관되며 놀랍지 않은 연산자 오버로드 묶음을 제공하려면 어느 정도 주의가 필요하며, 그러지 못하면 혼란과 버그로 이어질 수 있습니다.
  • 연산자를 과용하면 코드를 알아보기 어려워집니다. 오버로드된 연산자의 의미가 관례를 따르지 않을 때 특히 그렇습니다.
  • 함수 오버로딩의 위험은 연산자 오버로딩에도 똑같이, 어쩌면 더 크게 적용됩니다.
  • 연산자 오버로드는 비싼 연산을 값싼 내장 연산이라고 착각하게 만들어 직관을 속일 수 있습니다.
  • 오버로드된 연산자의 호출 지점을 찾으려면 grep 같은 도구가 아니라 C++ 문법을 이해하는 검색 도구가 필요할 수 있습니다.
  • 오버로드된 연산자의 인수 타입을 잘못 쓰면 컴파일 오류가 아니라 다른 오버로드가 선택될 수 있습니다. 예를 들어 foo < bar는 어떤 일을 하는데 &foo < &bar는 전혀 다른 일을 할 수 있습니다.
  • 어떤 연산자 오버로드는 본질적으로 위험합니다. 단항 &를 오버로드하면 그 오버로드 선언이 보이는지 여부에 따라 같은 코드가 다른 의미를 가질 수 있습니다. &&, ||, ,(쉼표)의 오버로드는 내장 연산자의 평가 순서 의미를 흉내 낼 수 없습니다.
  • 연산자는 클래스 바깥에 정의되는 경우가 많아서, 서로 다른 파일이 같은 연산자에 대해 서로 다른 정의를 들여올 위험이 있습니다. 두 정의가 같은 바이너리에 링크되면 정의되지 않은 동작이 되며, 미묘한 런타임 버그로 나타날 수 있습니다.
  • 사용자 정의 리터럴(UDL)은 숙련된 C++ 프로그래머에게조차 낯선 새로운 문법 형태를 만들어 냅니다. std::string_view("Hello World")를 줄인 "Hello World"sv 같은 것이 그 예입니다. 기존 표기법은 덜 간결하지만 더 명확합니다.
  • UDL은 네임스페이스로 한정할 수 없기 때문에, 사용하려면 using 지시문(금지되어 있습니다)이나 using 선언(헤더 파일에서는 금지되어 있습니다. 단, 가져온 이름이 그 헤더가 노출하는 인터페이스의 일부인 경우는 예외)이 필요합니다. 헤더 파일이 UDL 접미사를 피해야 한다는 점을 생각하면, 헤더 파일과 소스 파일에서 리터럴 관례가 달라지는 것을 피하는 편이 낫습니다.

결정:

오버로드된 연산자는 그 의미가 명백하고, 놀랍지 않으며, 대응하는 내장 연산자와 일관될 때만 정의하세요. 예를 들어 |는 셸 스타일의 파이프가 아니라 비트 OR이나 논리 OR로 사용하세요.

연산자는 여러분 자신의 타입에만 정의하세요. 더 정확히는, 그 연산자가 다루는 타입과 같은 헤더, 같은 .cc 파일, 같은 네임스페이스에 정의하세요. 그래야 타입이 있는 곳이면 어디서나 연산자를 쓸 수 있어 정의가 여러 개가 될 위험이 최소화됩니다. 가능하면 연산자를 템플릿으로 정의하지 마세요. 가능한 모든 템플릿 인수에 대해 이 규칙을 만족해야 하기 때문입니다. 연산자를 하나 정의했다면 의미가 통하는 관련 연산자들도 함께 정의하고, 서로 일관되게 정의되었는지 확인하세요.

값을 수정하지 않는 이항 연산자는 비멤버 함수로 정의하는 것을 선호하세요. 이항 연산자를 클래스 멤버로 정의하면 암시적 변환이 오른쪽 인수에는 적용되지만 왼쪽 인수에는 적용되지 않습니다. a + b는 컴파일되는데 b + a는 되지 않는다면 사용자가 혼란스러울 것입니다.

값의 동등 비교가 가능한 타입 T에는 비멤버 operator==를 정의하고, 타입 T의 두 값이 언제 같다고 보는지를 문서화하세요. 타입 T의 값 t1이 다른 값 t2보다 작다는 것에 대해 명백한 개념이 단 하나 있다면 operator<=>도 정의할 수 있으며, 이는 operator==와 일관되어야 합니다. 그 밖의 비교·순서 연산자는 오버로드하지 않는 편을 선호하세요.

연산자 오버로드를 정의하지 않으려고 일부러 애쓰지는 마세요. 예를 들어 Equals(), CopyFrom(), PrintTo()보다 ==, =, <<를 정의하는 편이 낫습니다. 반대로, 다른 라이브러리가 기대한다는 이유만으로 연산자 오버로드를 정의하지도 마세요. 예를 들어 여러분의 타입에 자연스러운 순서가 없는데 그것을 std::set에 저장하고 싶다면, <를 오버로드하는 대신 사용자 정의 비교자를 사용하세요.

&&, ||, ,(쉼표), 단항 &는 오버로드하지 마세요. operator""도 오버로드하지 마세요. 즉, 사용자 정의 리터럴을 도입하지 마세요. 다른 사람이 제공하는(표준 라이브러리가 제공하는 것을 포함해) 그런 리터럴도 사용하지 마세요.

타입 변환 연산자는 암시적 변환 절에서 다룹니다. = 연산자는 복사 생성자 절에서 다룹니다. 스트림과 함께 쓰기 위한 << 오버로딩은 스트림 절에서 다룹니다. 연산자 오버로딩에도 적용되는 함수 오버로딩 규칙도 참조하세요.


옮긴이 풀이

핵심: 연산자는 "신중하게", 사용자 정의 리터럴은 "금지"

연산자 오버로딩은 사용자 타입을 내장 타입처럼 쓰게 해 코드를 간결하게 만들지만, 남용하면 "싸 보이는데 실제로는 비싼 연산", "어떤 함수가 불리는지 모호함" 같은 함정을 만듭니다.

규칙 요약

  • 의미가 명확하고, 놀랍지 않고, 내장 연산자와 일치할 때만 오버로드하세요. (예: |는 비트/논리 OR이지 셸 파이프가 아닙니다.)
  • 자신의 타입에 대해서만 정의하세요(같은 헤더·.cc·네임스페이스). 가능하면 템플릿으로 정의하지 마세요.
  • 한 연산자를 정의하면 관련 연산자도 일관되게 정의하세요.
  • 수정하지 않는 이항 연산자는 비멤버 함수로 정의하는 것을 선호하세요. 멤버로 두면 왼쪽 인수에는 암시적 변환이 안 되어 a + b는 되는데 b + a는 안 되는 혼란이 생깁니다.
  • 동등 비교가 가능하면 비멤버 operator==를 정의하고, 명확한 순서 개념이 하나뿐이면 operator<=>를 operator==와 일치하게 정의할 수 있습니다.

하지 말아야 할 것

  • &&, ||, ,(쉼표), 단항 & 오버로드 → 내장 연산자의 평가 순서 의미를 흉내 낼 수 없음
  • operator"" 오버로드(사용자 정의 리터럴, UDL) → 도입도, 사용도 하지 마세요(표준 라이브러리가 제공하는 리터럴 포함)
  • 오버로드를 피하려고 Equals(), CopyFrom()을 억지로 쓰는 것도, 반대로 다른 라이브러리가 기대한다고 억지로 <를 오버로드하는 것도 피하세요(후자는 사용자 정의 비교자를 쓰세요).

다른 곳에서 다루는 연산자

타입 변환 연산자는 암시적 변환, =는 복사 생성자, 스트림용 <<는 스트림 절에서 다룹니다.