operator overloading
사용자 정의 타입의 피연산자에 대한 C++ 연산자를 사용자 정의합니다.
목차 |
구문
연산자 함수 는 특별한 함수 이름을 가진 함수 입니다:
operator
op
|
(1) | ||||||||
operator
new
operator
new []
|
(2) | ||||||||
operator
delete
operator
delete []
|
(3) | ||||||||
operator
co_await
|
(4) | (C++20부터) | |||||||
| op | - | 다음 연산자 중 하나: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (C++20부터) && || ++ -- , - > * - > ( ) [ ] |
구두점이 아닌 연산자들의 동작은 각각의 페이지에서 설명됩니다. 달리 명시되지 않는 한, 이 페이지의 나머지 설명은 이러한 함수들에 적용되지 않습니다.
설명
연산자가 표현식 에 나타날 때, 그리고 피연산자 중 적어도 하나가 클래스 타입 이거나 열거형 타입 인 경우, 오버로드 해결 을 사용하여 다음 조건과 시그니처가 일치하는 모든 함수 중에서 호출할 사용자 정의 함수를 결정합니다:
| 표현식 | 멤버 함수로 사용 시 | 비멤버 함수로 사용 시 | 예시 |
|---|---|---|---|
| @a | (a).operator@ ( ) | operator@ (a) | ! std:: cin 호출 std:: cin . operator ! ( ) |
| a@b | (a).operator@ (b) | operator@ (a, b) | std:: cout << 42 호출 std:: cout . operator << ( 42 ) |
| a=b | (a).operator= (b) | 비멤버로 사용 불가 | 주어진 std:: string s ; , s = "abc" ; 호출 s. operator = ( "abc" ) |
| a(b...) | (a).operator()(b...) | 비멤버로 사용 불가 | 주어진 std:: random_device r ; , auto n = r ( ) ; 호출 r. operator ( ) ( ) |
| a[b...] | (a).operator[](b...) | 비멤버로 사용 불가 | 주어진 std:: map < int , int > m ; , m [ 1 ] = 2 ; 호출 m. operator [ ] ( 1 ) |
| a-> | (a).operator->( ) | 비멤버로 사용 불가 | 주어진 std:: unique_ptr < S > p ; , p - > bar ( ) 호출 p. operator - > ( ) |
| a@ | (a).operator@ (0) | operator@ (a, 0) | 주어진 std:: vector < int > :: iterator i ; , i ++ 호출 i. operator ++ ( 0 ) |
|
이 표에서
|
|||
|
또한 비교 연산자 == , ! = , < , > , <= , >= , <=> 에 대해, 오버로드 해결은 또한 재작성된 후보 operator == 또는 operator <=> 도 고려합니다. |
(C++20부터) |
오버로드된 연산자들(그러나 내장 연산자들은 제외)은 함수 표기법을 사용하여 호출할 수 있습니다:
std::string str = "Hello, "; str.operator+=("world"); // str += "world";와 동일 operator<<(operator<<(std::cout, str), '\n'); // std::cout << str << '\n';와 동일 // (C++17부터) 시퀀싱을 제외하고
정적 오버로드된 연산자멤버 함수인 오버로드된 연산자는 static 으로 선언될 수 있습니다. 그러나 이는 operator ( ) 과 operator [ ] 에 대해서만 허용됩니다. 이러한 연산자는 함수 표기법을 사용하여 호출할 수 있습니다. 그러나 이러한 연산자가 표현식에 나타날 때는 여전히 클래스 타입의 객체가 필요합니다. struct SwapThem { template<typename T> static void operator()(T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } template<typename T> static void operator[](T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } }; inline constexpr SwapThem swap_them{}; void foo() { int a = 1, b = 2; swap_them(a, b); // OK swap_them[a, b]; // OK SwapThem{}(a, b); // OK SwapThem{}[a, b]; // OK SwapThem::operator()(a, b); // OK SwapThem::operator[](a, b); // OK SwapThem(a, b); // error, invalid construction SwapThem[a, b]; // error } |
(C++23부터) |
제한 사항
- 연산자 함수는 반드시 클래스, 클래스에 대한 참조, 열거형, 또는 열거형에 대한 참조 타입인 함수 매개변수나 암시적 객체 매개변수를 하나 이상 가져야 합니다.
-
::(범위 지정),.(멤버 접근),.*(멤버 포인터를 통한 멤버 접근),?:(삼항 조건) 연산자는 오버로드할 수 없습니다. -
**,<>,&|와 같은 새로운 연산자를 생성할 수 없습니다. - 연산자의 우선순위, 그룹화, 또는 피연산자 개수를 변경할 수 없습니다.
-
->연산자 오버로드는 반드시 원시 포인터를 반환하거나,->연산자가 다시 오버로드된 객체(참조 또는 값으로)를 반환해야 합니다. -
&&와||연산자 오버로드는 단락 평가(short-circuit evaluation)를 상실합니다.
|
(C++17 이전) |
표준 구현
위의 제한 사항 외에도, 언어는 오버로드된 연산자가 수행하는 작업이나 반환 타입에 대해 다른 제약을 두지 않지만(이는 오버로드 해결에 참여하지 않음), 일반적으로 오버로드된 연산자는 내장 연산자와 최대한 유사하게 동작할 것으로 기대됩니다: operator + 는 인수를 곱하는 대신 더할 것으로 기대되며, operator = 는 할당 등을 수행할 것으로 기대됩니다. 관련 연산자들도 유사하게 동작할 것으로 기대됩니다( operator + 와 operator + = 는 동일한 덧셈 연산을 수행함). 반환 타입은 연산자가 사용될 것으로 예상되는 표현식에 의해 제한됩니다: 예를 들어, 할당 연산자는 참조로 반환하여 a = b = c = d 와 같은 작성이 가능하도록 합니다. 왜냐하면 내장 연산자들이 이를 허용하기 때문입니다.
일반적으로 오버로드되는 연산자들은 다음과 같은 전형적이고 표준적인 형태를 가집니다: [1]
대입 연산자
대입 연산자 operator = 는 특별한 속성을 가집니다: 자세한 내용은 복사 대입 과 이동 대입 을 참조하십시오.
표준 복사 할당 연산자는 자기 할당에 안전해야 하며 , 좌변을 참조로 반환해야 합니다:
// 복사 대입 T& operator=(const T& other) { // 자기 대입 방지 if (this == &other) return *this; // *this가 힙에 할당된 버퍼 mArray와 같은 재사용 가능한 리소스를 관리한다고 가정 if (size != other.size) // *this의 리소스를 재사용할 수 없음 { temp = new int[other.size]; // 리소스 할당, 예외 발생 시 아무 작업도 수행하지 않음 delete[] mArray; // *this의 리소스 해제 mArray = temp; size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); return *this; }
|
표준 이동 대입 연산자는 이동된 객체를 유효한 상태로 남겨둘 것 (즉, 클래스 불변식을 유지한 상태)을 기대하며, 아무 작업도 수행하지 않거나 최소한 자기 대입 시 객체를 유효한 상태로 남기고, 비상수 참조로 좌변을 반환하며, noexcept여야 합니다: // move assignment T& operator=(T&& other) noexcept { // Guard self assignment if (this == &other) return *this; // delete[]/size=0 would also be ok delete[] mArray; // release resource in *this mArray = std::exchange(other.mArray, nullptr); // leave other in valid state size = std::exchange(other.size, 0); return *this; } |
(since C++11) |
리소스 재사용의 이점을 얻을 수 없는 복사 할당 상황에서(힙 할당 배열을 관리하지 않거나, std::vector 나 std::string 과 같이 (전이적으로) 해당 기능을 가진 멤버가 없는 경우), 널리 사용되는 편리한 단축 표현이 있습니다: 복사-후-교환 할당 연산자로, 매개변수를 값으로 받아들여(따라서 인자의 값 범주에 따라 복사 및 이동 할당 모두로 작동), 매개변수와 교환한 후 소멸자가 정리하도록 합니다.
이 폼은 자동으로 strong exception guarantee 를 제공하지만, 자원 재사용은 금지합니다.
스트림 추출 및 삽입
operator>>
와
operator<<
의 오버로드 중
std::
istream
&
또는
std::
ostream
&
를 좌측 인자로 취하는 것들은 삽입(insertion) 및 추출(extraction) 연산자로 알려져 있습니다. 이들은 사용자 정의 타입을 우측 인자(
b
in
a @ b
)로 취하기 때문에 비멤버 함수로 구현되어야 합니다.
std::ostream& operator<<(std::ostream& os, const T& obj) { // 객체를 스트림에 기록 return os; } std::istream& operator>>(std::istream& is, T& obj) { // 스트림에서 객체 읽기 if (/* T를 생성할 수 없는 경우 */) is.setstate(std::ios::failbit); return is; }
이러한 연산자는 때때로 friend functions 로 구현됩니다.
함수 호출 연산자
사용자 정의 클래스가 함수 호출 연산자 operator ( ) 를 오버로드하면, 이는 FunctionObject 타입이 됩니다.
이러한 타입의 객체는 함수 호출 표현식에서 사용될 수 있습니다:
// 이 타입의 객체는 일변수 선형 함수 a * x + b를 나타냅니다. struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // 함수 2x + 1을 나타냅니다. Linear g{-1, 0}; // 함수 -x를 나타냅니다. // f와 g는 함수처럼 사용할 수 있는 객체입니다. double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
많은 표준 라이브러리 알고리즘 들은 동작을 사용자 정의하기 위해 FunctionObject s 를 받아들입니다. operator ( ) 의 특별히 주목할 만한 표준 형식은 없지만, 사용법을 설명하기 위해:
#include <algorithm> #include <iostream> #include <vector> struct Sum { int sum = 0; void operator()(int n) { sum += n; } }; int main() { std::vector<int> v = {1, 2, 3, 4, 5}; Sum s = std::for_each(v.begin(), v.end(), Sum()); std::cout << "The sum is " << s.sum << '\n'; }
출력:
The sum is 15
증가 및 감소
후위 증가 또는 감소 연산자가 표현식에 나타날 때, 해당 사용자 정의 함수( operator ++ 또는 operator -- )가 정수 인수 0 를 사용하여 호출됩니다. 일반적으로 이는 T operator ++ ( int ) 또는 T operator -- ( int ) 로 선언되며, 이 인수는 무시됩니다. 후위 증가 및 감소 연산자는 일반적으로 전위 버전을 사용하여 구현됩니다:
struct X { // 전위 증가 연산자 X& operator++() { // 실제 증가 연산이 여기서 수행됨 return *this; // 새로운 값을 참조로 반환 } // 후위 증가 연산자 X operator++(int) { X old = *this; // 이전 값 복사 operator++(); // 전위 증가 연산 호출 return old; // 이전 값 반환 } // 전위 감소 연산자 X& operator--() { // 실제 감소 연산이 여기서 수행됨 return *this; // 새로운 값을 참조로 반환 } // 후위 감소 연산자 X operator--(int) { X old = *this; // 이전 값 복사 operator--(); // 전위 감소 연산 호출 return old; // 이전 값 반환 } };
접두사 증가 및 감소 연산자의 표준 구현은 참조로 반환하지만, 다른 연산자 오버로드와 마찬가지로 반환 타입은 사용자 정의입니다. 예를 들어 std::atomic 에 대한 이러한 연산자의 오버로드는 값으로 반환합니다.
이항 산술 연산자
이항 연산자는 일반적으로 대칭성을 유지하기 위해 비멤버 함수로 구현됩니다(예를 들어, 복소수와 정수를 더할 때, operator + 가 복소수 타입의 멤버 함수라면 complex + integer 만 컴파일되고 integer + complex 는 컴파일되지 않습니다). 모든 이항 산술 연산자에 대해 해당 복합 할당 연산자가 존재하므로, 이항 연산자의 표준 형식은 해당 복합 할당 연산자를 사용하여 구현됩니다:
class X { public: X& operator+=(const X& rhs) // 복합 할당 (멤버일 필요는 없지만, { // private 멤버를 수정하기 위해 종종 멤버로 선언됨) /* rhs를 *this에 더하는 작업이 여기서 수행됨 */ return *this; // 참조로 결과 반환 } // 클래스 내부에서 정의된 friend 함수는 인라인이며 비-ADL 조회에서 숨겨짐 friend X operator+(X lhs, // lhs를 값으로 전달하면 a+b+c 같은 연쇄 연산 최적화에 도움 const X& rhs) // 그렇지 않으면 두 매개변수 모두 const 참조일 수 있음 { lhs += rhs; // 복합 할당 재사용 return lhs; // 값으로 결과 반환 (이동 생성자 사용) } };
비교 연산자
표준 라이브러리 알고리즘인 std::sort 와 컨테이너인 std::set 은 사용자 정의 타입에 대해 기본적으로 operator < 가 정의되어 있을 것을 기대하며, 이 연산자가 엄격한 약순서(strict weak ordering)를 구현할 것을 요구합니다(따라서 Compare 요구 사항을 충족해야 합니다). 구조체에 대한 엄격한 약순서를 구현하는 관용적인 방법은 std::tie 가 제공하는 사전식 비교(lexicographical comparison)를 사용하는 것입니다:
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // 동일한 순서 유지 } };
일반적으로 operator < 가 제공되면, 다른 관계 연산자들은 operator < 를 기반으로 구현됩니다.
inline bool operator< (const X& lhs, const X& rhs) { /* 실제 비교 수행 */ } inline bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }
마찬가지로, 부등식 연산자는 일반적으로 operator == 를 기반으로 구현됩니다:
inline bool operator==(const X& lhs, const X& rhs) { /* 실제 비교 수행 */ } inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }
삼방 비교(예: std::memcmp 또는 std::string::compare )가 제공될 때, 여섯 가지의 양방 비교 연산자 모두 이를 통해 표현될 수 있습니다:
inline bool operator==(const X& lhs, const X& rhs) { return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs) { return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs) { return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) >= 0; }
배열 첨자 연산자
배열과 같은 읽기 및 쓰기 접근을 제공하는 사용자 정의 클래스는 일반적으로 const 및 비 const 변형인 operator [ ] 에 대해 두 개의 오버로드를 정의합니다:
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
|
또는 다음과 같이 명시적 객체 매개변수 를 사용하는 단일 멤버 함수 템플릿으로 표현할 수 있습니다: struct T { decltype(auto) operator[](this auto& self, std::size_t idx) { return self.mVector[idx]; } }; |
(C++23부터) |
값 타입이 스칼라 타입으로 알려진 경우, const 변형은 값으로 반환해야 합니다.
컨테이너 요소에 대한 직접 접근이 바람직하지 않거나 불가능한 경우, 또는 lvalue c [ i ] = v ; 와 rvalue v = c [ i ] ; 사용을 구분해야 하는 경우, operator [ ] 는 프록시를 반환할 수 있습니다. 예를 들어 std::bitset::operator[] 를 참조하십시오.
|
operator [ ] 는 하나의 첨자만 받을 수 있습니다. 다차원 배열 접근 의미론을 제공하기 위해, 예를 들어 3차원 배열 접근 a [ i ] [ j ] [ k ] = x ; 을 구현하려면, operator [ ] 가 2차원 평면에 대한 참조를 반환해야 하며, 이는 자체적으로 operator [ ] 를 가져 1차원 행에 대한 참조를 반환해야 하고, 다시 이 행은 요소에 대한 참조를 반환하는 operator [ ] 를 가져야 합니다. 이러한 복잡성을 피하기 위해 일부 라이브러리는 operator ( ) 를 오버로딩하여 3차원 접근 표현식이 Fortran 스타일의 구문 a ( i, j, k ) = x ; 을 갖도록 합니다. |
(C++23 이전) |
|
operator [ ] 는 임의의 개수의 첨자를 받을 수 있습니다. 예를 들어, 3차원 배열 클래스의 operator [ ] 가 T & operator [ ] ( std:: size_t x, std:: size_t y, std:: size_t z ) ; 로 선언된 경우 요소에 직접 접근할 수 있습니다.
이 코드 실행
#include <array> #include <cassert> #include <iostream> template<typename T, std::size_t Z, std::size_t Y, std::size_t X> struct Array3d { std::array<T, X * Y * Z> m{}; constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23 { assert(x < X and y < Y and z < Z); return m[z * Y * X + y * X + x]; } }; int main() { Array3d<int, 4, 3, 2> v; v[3, 2, 1] = 42; std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n'; } 출력: v[3, 2, 1] = 42 |
(C++23 이후) |
비트 연산자
BitmaskType 요구 사항을 구현하는 사용자 정의 클래스 및 열거형은 비트 연산자 operator & , operator | , operator ^ , operator~ , operator & = , operator | = , 그리고 operator ^ = 를 오버로드해야 하며, 선택적으로 시프트 연산자 operator << operator >> , operator >>= , 그리고 operator <<= 를 오버로드할 수 있습니다. 표준 구현은 일반적으로 위에서 설명한 이항 산술 연산자 패턴을 따릅니다.
불리언 부정 연산자
|
operator ! 연산자는 일반적으로 불린 컨텍스트에서 사용될 사용자 정의 클래스들에 의해 오버로드됩니다. 이러한 클래스들은 또한 불린 타입으로의 사용자 정의 변환 함수를 제공하며(표준 라이브러리 예제는 std::basic_ios 참조), operator ! 의 기대되는 동작은 operator bool 의 반대 값을 반환하는 것입니다. |
(C++11 이전) |
|
내장 연산자 ! 가 bool 으로의 상황별 변환 을 수행하기 때문에, 불린 컨텍스트에서 사용될 사용자 정의 클래스들은 operator bool 만 제공하면 되며 operator ! 을 오버로드할 필요가 없습니다. |
(C++11 이후) |
거의 오버로드되지 않는 연산자
다음 연산자는 거의 오버로드되지 않습니다:
-
주소 연산자,
operator
&
. 단항 &가 불완전한 타입의 lvalue에 적용되고 완전한 타입이 오버로드된
operator
&
를 선언하는 경우, 연산자가 내장 의미를 가지는지 또는 연산자 함수가 호출되는지는 명시되지 않습니다. 이 연산자는 오버로드될 수 있으므로, 일반화 라이브러리들은 사용자 정의 타입 객체의 주소를 얻기 위해
std::addressof
를 사용합니다. 가장 잘 알려진 표준적인 오버로드된
operator
&
의 예는 Microsoft 클래스
CComPtrBase입니다. EDSL에서 이 연산자의 사용 예는 boost.spirit 에서 찾을 수 있습니다. - 불리언 논리 연산자, operator && 와 operator || . 내장 버전과 달리, 오버로드된 버전은 단락 평가(short-circuit evaluation)를 구현할 수 없습니다. 또한 내장 버전과 달리, 왼쪽 피연산자를 오른쪽 피연산자보다 먼저 시퀀싱하지 않습니다. (C++17까지) 표준 라이브러리에서 이러한 연산자는 std::valarray 에 대해서만 오버로드됩니다.
- 쉼표 연산자, operator, . 내장 버전과 달리, 오버로드된 버전은 왼쪽 피연산자를 오른쪽 피연산자보다 먼저 시퀀싱하지 않습니다. (C++17까지) 이 연산자는 오버로드될 수 있으므로, 일반화 라이브러리들은 사용자 정의 타입의 표현식 실행을 시퀀싱하기 위해 a, void ( ) , b 와 같은 표현식을 a, b 대신 사용합니다. boost 라이브러리는 operator, 를 boost.assign , boost.spirit 및 다른 라이브러리에서 사용합니다. 데이터베이스 접근 라이브러리 SOCI 또한 operator, 를 오버로드합니다.
- 멤버 포인터를 통한 멤버 접근 operator - > * . 이 연산자를 오버로드하는 것에 특별한 단점은 없지만, 실제로는 거의 사용되지 않습니다. 이것이 스마트 포인터 인터페이스 의 일부가 될 수 있다고 제안되었으며, 실제로 boost.phoenix 의 액터들이 그 용도로 사용합니다. cpp.react 와 같은 EDSL에서 더 일반적으로 사용됩니다.
참고 사항
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_static_call_operator
|
202207L
|
(C++23) | static operator ( ) |
__cpp_multidimensional_subscript
|
202211L
|
(C++23) | static operator [ ] |
키워드
예제
#include <iostream> class Fraction { // or C++17's std::gcd constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {} constexpr int num() const { return n; } constexpr int den() const { return d; } constexpr Fraction& operator*=(const Fraction& rhs) { int new_n = n * rhs.n / gcd(n * rhs.n, d * rhs.d); d = d * rhs.d / gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.num() << '/' << f.den(); } constexpr bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } constexpr Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { constexpr Fraction f1{3, 8}, f2{1, 2}, f3{10, 2}; std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; static_assert(f3 == f2 * 10); }
출력:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
결함 보고서
다음의 동작 변경 결함 보고서들은 이전에 발표된 C++ 표준에 소급 적용되었습니다.
| DR | 적용 대상 | 게시된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1481 | C++98 |
비멤버 접두 증가 연산자는 클래스 타입, 열거형 타입,
또는 해당 타입에 대한 참조 타입의 매개변수만 가질 수 있었음 |
타입 제한 없음 |
| CWG 2931 | C++23 |
명시적 객체 멤버 연산자 함수는 클래스 타입, 열거형 타입,
또는 해당 타입에 대한 참조 타입의 매개변수를 가질 수 없었음 |
금지됨 |
참고 항목
| 일반 연산자 | ||||||
|---|---|---|---|---|---|---|
| assignment |
increment
decrement |
arithmetic | logical | comparison |
member
access |
other |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
함수 호출
a ( ... ) |
|
콤마
a, b |
||||||
|
조건부 연산자
a ? b : c |
||||||
| 특수 연산자 | ||||||
|
static_cast
관련된 타입 간 변환을 수행
|
||||||