콘텐츠로 이동

연산자 오버로딩 (Operator Overloading)

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

C++에서는 매개 변수 중 하나가 사용자 정의 형식인 경우 사용자 코드에서 연산자 키워드를 사용하여 내장 연산자의 오버로드된 버전을 선언할 수 있습니다. 또한 연산자 키워드를 사용하면 사용자 코드는 연산자"" 를 사용하여 새로운 종류의 리터럴을 정의하고 연산자 bool() 과 같은 유형 변환 함수를 정의할 수 있습니다.

연산자 오버로딩은 사용자 정의 유형이 내장 유형과 동일하게 작동하도록 하여 코드를 더욱 간결하고 직관적으로 만들 수 있습니다. 오버로드된 연산자는 특정 작업(예: == , < , = 및 << )에 대한 관용적 이름이며 이러한 규칙을 준수하면 사용자 정의 유형을 더 읽기 쉽게 만들고 해당 이름을 예상하는 라이브러리와 상호 운용할 수 있습니다.

사용자 정의 리터럴은 사용자 정의 유형의 개체를 생성하기 위한 매우 간결한 표기법입니다.

  • 정확하고 일관되며 놀라운 연산자 오버로드 세트를 제공하려면 약간의 주의가 필요하며, 그렇게 하지 않으면 혼란과 버그가 발생할 수 있습니다.
  • 연산자를 과도하게 사용하면 코드가 난독화될 수 있으며, 특히 오버로드된 연산자의 의미 체계가 규칙을 따르지 않는 경우 더욱 그렇습니다.
  • 기능 과부하의 위험은 연산자 과부하에도 똑같이 적용됩니다.
  • 연산자 오버로드는 비용이 많이 드는 작업이 값싸고 내장된 작업이라고 생각하도록 직관을 속일 수 있습니다.
  • 오버로드된 연산자에 대한 호출 사이트를 찾으려면 grep 등이 아닌 C++ 구문을 인식하는 검색 도구가 필요할 수 있습니다.
  • 오버로드된 연산자의 인수 유형이 잘못되면 컴파일러 오류가 아닌 다른 오버로드가 발생할 수 있습니다. 예를 들어 foo < bar는 한 가지 작업을 수행하는 반면 &foo < &bar는 완전히 다른 작업을 수행합니다.
  • 특정 운영자 과부하는 본질적으로 위험합니다. 단항 &를 오버로드하면 오버로드 선언이 표시되는지 여부에 따라 동일한 코드가 다른 의미를 가질 수 있습니다. && 오버로드, || , 및 ,(쉼표)는 내장 연산자의 평가 순서 의미와 일치할 수 없습니다.
  • 연산자는 클래스 외부에서 정의되는 경우가 많으므로 동일한 연산자에 대한 다른 정의가 다른 파일에 도입될 위험이 있습니다. 두 정의가 모두 동일한 바이너리에 연결되면 정의되지 않은 동작이 발생하여 미묘한 런타임 버그로 나타날 수 있습니다.
  • UDL(사용자 정의 리터럴)을 사용하면 숙련된 C++ 프로그래머에게도 익숙하지 않은 새로운 구문 형식(예: std::string_view("Hello World") 의 약어인 "Hello World"sv)을 생성할 수 있습니다. 기존 표기법은 덜 간결하지만 더 명확합니다.
  • 네임스페이스 한정이 불가능하기 때문에 UDL을 사용하려면 using 지시문(금지) 또는 using-선언(가져온 이름이 문제의 헤더 파일에 의해 노출되는 인터페이스의 일부인 경우를 제외하고 헤더 파일에서 금지)을 사용해야 합니다. 헤더 파일이 UDL 접미사를 피해야 한다는 점을 고려하면 헤더 파일과 소스 파일 간에 리터럴에 대한 규칙이 달라지는 것을 피하는 것이 좋습니다.

의미가 명확하고, 놀랍지 않으며, 해당 기본 제공 연산자와 일치하는 경우에만 오버로드된 연산자를 정의하십시오. 예를 들어 | 쉘 스타일 파이프가 아닌 비트별 또는 논리형 파이프로 사용됩니다.

자신의 유형에만 연산자를 정의하십시오. 보다 정확하게는 동일한 헤더, .cc 파일 및 네임스페이스에서 작동하는 유형으로 정의하십시오. 이렇게 하면 유형이 어디에 있든 연산자를 사용할 수 있으므로 여러 정의로 인한 위험이 최소화됩니다. 가능한 경우 연산자를 템플릿으로 정의하지 마세요. 왜냐하면 가능한 모든 템플릿 인수에 대해 이 규칙을 충족해야 하기 때문입니다. 연산자를 정의하는 경우 관련 연산자도 정의하고 일관되게 정의되었는지 확인하세요.

수정되지 않는 이항 연산자를 비멤버 함수로 정의하는 것을 선호합니다. 이항 연산자가 클래스 멤버로 정의된 경우 암시적 변환은 오른쪽 인수에는 적용되지만 왼쪽 인수에는 적용되지 않습니다. a + b는 컴파일되지만 b + a는 그렇지 않으면 사용자에게 혼란을 줄 것입니다.

값이 같은지 비교할 수 있는 T 유형의 경우 비멤버 연산자==를 정의하고 T 유형의 두 값이 동일한 것으로 간주되는 경우를 문서화합니다. T 유형의 값 t1이 다른 값 t2보다 작은 경우에 대한 단 하나의 명확한 개념이 있는 경우에는 Operator==와 일치해야 하는 Operator<=>를 정의할 수도 있습니다. 다른 비교 및 ​​정렬 연산자를 오버로드하지 않는 것이 좋습니다.

연산자 오버로드 정의를 피하기 위해 애쓰지 마십시오. 예를 들어 Equals() , CopyFrom() 및 PrintTo() 보다 == , = 및 << 을 정의하는 것이 좋습니다. 반대로, 다른 라이브러리에서 예상한다고 해서 연산자 오버로드를 정의하지 마세요. 예를 들어 유형에 자연 순서가 없지만 이를 std::set 에 저장하려는 경우 < 을 오버로드하는 대신 사용자 정의 비교기를 사용하십시오.

&&를 오버로드하지 마세요, || , , (쉼표) 또는 단항 & . 연산자"" 를 오버로드하지 마세요. 즉, 사용자 정의 리터럴을 도입하지 마세요. 다른 사람(표준 라이브러리 포함)에서 제공하는 리터럴을 사용하지 마세요.

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


이해하기 쉽게 설명하기

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

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

규칙 요약

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

하지 말아야 할 것

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

다른 곳에서 다루는 연산자

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