운영자 과부하에 대한 기본 규칙과 관용구는 무엇입니까?


2141

참고 : 답변은 특정 순서대로 제공 되었지만 많은 사용자가 답변을받은 시간이 아니라 투표에 따라 답변을 정렬하므로 가장 적합한 순서대로 답변 색인을 제공합니다 .

(참고 : 이것은 Stack Overflow의 C ++ FAQ에 대한 항목 입니다.이 양식으로 FAQ를 제공한다는 아이디어를 비판하려면이 모든 것을 시작한 메타에 게시 하면됩니다. 이 질문은 C ++ 대화방 에서 모니터링되며 여기서 FAQ 아이디어는 처음부터 시작되었으므로 아이디어를 얻은 사람들이 대답을 읽을 가능성이 큽니다.)


63
C ++-FAQ 태그를 계속 사용하려면 항목의 형식을 지정해야합니다.
John Dibling

나는 연산자 오버로딩에 관한 독일 C ++ 커뮤니티를위한 짧은 시리즈의 글을 썼다 : 1 부 : C ++에서의 연산자 오버로딩은 모든 연산자에 대한 의미, 일반적인 사용법 및 전문을 다룬다. 여기에는 귀하의 답변과 겹치는 부분이 있지만 추가 정보가 있습니다. 파트 2와 3은 Boost.Operators 사용을위한 학습서를 작성합니다. 번역하고 답변으로 추가 하시겠습니까?
Arne Mertz

아, 그리고 영어 번역도 가능합니다 : 기초일반적인 연습
Arne Mertz

답변:


1042

과부하를 일으키는 일반적인 연산자

과부하 연산자의 대부분의 작업은 보일러 플레이트 코드입니다. 연산자가 단순히 구문 설탕이기 때문에 실제 작업이 일반 기능으로 수행 될 수 있으며 종종 전달됩니다. 그러나이 보일러 플레이트 코드를 올바르게 얻는 것이 중요합니다. 실패하면 운영자 코드가 컴파일되지 않거나 사용자 코드가 컴파일되지 않거나 사용자 코드가 놀랍게 동작합니다.

할당 연산자

과제에 대해 할 말이 많습니다. 그러나 대부분은 이미 GMan의 유명한 Copy-And-Swap FAQ 에서 언급 되었으므로 여기서는 대부분의 참조를 생략하고 참조를위한 완벽한 할당 연산자 만 나열합니다.

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

비트 시프트 연산자 (스트림 I / O에 사용)

비트 시프트 연산자 <<>>는 C에서 상속 한 비트 조작 함수를위한 하드웨어 인터페이스에 여전히 사용되지만 대부분의 응용 프로그램에서 오버로드 된 스트림 입력 및 출력 연산자로 널리 퍼져 있습니다. 비트 조작 연산자로서 오버로드에 대한 지침은 이진 산술 연산자에 대한 아래 섹션을 참조하십시오. 객체가 iostream과 함께 사용될 때 고유 한 사용자 지정 형식 및 구문 분석 논리를 구현하려면 계속하십시오.

가장 일반적으로 오버로드 된 연산자 중 스트림 연산자는 이진 삽입 연산자이며 구문은 멤버인지 멤버가 아닌지에 대한 제한을 지정하지 않습니다. 왼쪽 인수를 변경하기 때문에 (스트림의 상태를 변경 함) 경험 법칙에 따라 왼쪽 피연산자 유형의 멤버로 구현해야합니다. 그러나 왼쪽 피연산자는 표준 라이브러리의 스트림이며 표준 라이브러리에 의해 정의 된 대부분의 스트림 출력 및 입력 연산자는 실제로 자신의 유형에 대한 출력 및 입력 조작을 구현할 때 스트림 클래스의 멤버로 정의됩니다. 표준 라이브러리의 스트림 유형을 변경할 수 없습니다. 따라서 멤버가 아닌 함수로 자신의 유형에 대해 이러한 연산자를 구현해야합니다. 두 가지의 정식 형태는 다음과 같습니다.

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // write obj to stream

  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // read obj from stream

  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);

  return is;
}

를 구현할 때 operator>>스트림 상태를 수동으로 설정하는 것은 읽기 자체가 성공한 경우에만 필요하지만 결과는 예상과 다릅니다.

함수 호출 연산자

함수 함수라고도하는 함수 객체를 만드는 데 사용되는 함수 호출 연산자는 멤버 함수 로 정의되어야 하므로 항상 this멤버 함수 의 암시 적 인수를 갖습니다 . 이 외에는 0을 포함하여 여러 개의 추가 인수를 사용하도록 오버로드 될 수 있습니다.

구문의 예는 다음과 같습니다.

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

용법:

foo f;
int a = f("hello");

C ++ 표준 라이브러리 전체에서 함수 객체는 항상 복사됩니다. 따라서 자신의 함수 객체는 복사하기에 저렴해야합니다. 함수 객체가 복사하는 데 비용이 많이 드는 데이터를 절대적으로 사용해야하는 경우 해당 데이터를 다른 곳에 저장하고 함수 객체가 참조하는 것이 좋습니다.

비교 연산자

이진 접두사 비교 연산자는 경험 법칙에 따라 비 멤버 함수 1 로 구현해야 합니다. 단항 접두사 부정 !은 (같은 규칙에 따라) 멤버 함수로 구현되어야합니다. (하지만 일반적으로 과부하는 좋지 않습니다.)

표준 라이브러리의 알고리즘 (예 :) std::sort()및 유형 (예 :) std::map은 항상 operator<존재할 것으로 예상 됩니다. 그러나 해당 유형사용자는 다른 모든 연산자도 존재할 것으로 예상 하므로 정의하는 경우 operator<연산자 오버로드의 세 번째 기본 규칙을 따르고 다른 모든 부울 비교 연산자도 정의해야합니다. 그것들을 구현하는 정식 방법은 다음과 같습니다.

inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return  operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}

여기서 주목해야 할 중요한 것은이 연산자 중 두 개만 실제로 무언가를 수행하고 다른 작업자는 실제 작업을 수행하기 위해이 두 가지 중 하나에 인수를 전달하는 것입니다.

나머지 이진 부울 연산자 ( ||, &&) 를 오버로드하는 구문 은 비교 연산자의 규칙을 따릅니다. 그러나 이러한 2에 대한 합리적인 사용 사례를 찾을 가능성 은 거의 없습니다 .

1 모든 경험 법칙과 마찬가지로 때때로이 규칙을 어기는 이유가있을 수 있습니다. 그렇다면, 이진 비교 구성원에 대한 기능이 될 것입니다 연산자의 왼쪽 피연산자 잊지 마세요 *this, 필요로 const너무. 따라서 멤버 함수로 구현 된 비교 연산자에는 다음과 같은 서명이 있어야합니다.

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

( const끝 부분에 유의하십시오 .)

2 그것은 주목해야한다 내장의 버전 ||&&사용 바로 가기 의미. 사용자가 정의한 것 (메소드 호출의 구문 설탕이기 때문에)은 바로 가기 시맨틱을 사용하지 않습니다. 사용자는 이러한 연산자에 바로 가기 의미가있을 것으로 예상되며 코드에 따라 달라질 수 있으므로 절대로 정의하지 않는 것이 좋습니다.

산술 연산자

단항 산술 연산자

단항 증가 및 감소 연산자는 접두사와 접미사 형식으로 제공됩니다. 다른 것을 구별하기 위해 접미사 변형은 추가 더미 int 인수를 사용합니다. 증분 또는 감소에 과부하가 걸리면 항상 접두사 및 접미사 버전을 모두 구현해야합니다. 다음은 증가의 표준 구현입니다. 감소는 동일한 규칙을 따릅니다.

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

접미사 변형은 접두사 측면에서 구현됩니다. 또한 postfix는 추가 사본을 수행합니다. 2

단항 빼기 및 더하기 과부하는 흔하지 않으며 피하는 것이 가장 좋습니다. 필요한 경우 멤버 함수로 오버로드되어야합니다.

2 접미사 변형이 더 많은 작업을 수행하므로 접두사 변형보다 사용 효율이 떨어집니다. 이것은 일반적으로 접두사 증가보다 접두사 증가를 선호하는 좋은 이유입니다. 컴파일러는 일반적으로 내장 유형에 대한 추가 접미사 증가 작업을 최적화 할 수 있지만 사용자 정의 유형 (목록 반복자처럼 순진한 것으로 보일 수 있음)에 대해서도 동일한 작업을 수행하지 못할 수 있습니다. 일단 익숙해지면 내장 유형 i++++i아닌 경우 i유형을 변경할 때 기억하기가 매우 어려워 지므로 유형을 변경할 때 코드를 변경해야합니다. 항상 습관을들이는 것이 좋습니다 postfix가 명시 적으로 필요하지 않은 경우 접두사 증가를 사용합니다.

이진 산술 연산자

이진 산술 연산자의 경우 세 번째 기본 규칙 연산자 오버로드를 준수하는 것을 잊지 마십시오. 제공하는 경우 +, 제공하는 +=경우, 제공 -하지 않으면 생략하지 마십시오 -=. Andrew Koenig는 복합 할당을 가장 먼저 관찰했다고합니다. 연산자는 비 컴파운드의 기반으로 사용할 수 있습니다. 즉, 운전자가되는 +면에 구현되어 +=, -측면에서 구현 -=

우리의 경험칙에 따르면 +, 그 동반자는 비회원이어야하고 +=, 그들의 왼쪽 주장을 바꾸는 그들의 복합 할당 상대방 ( 등)은 회원이어야합니다. 여기에 대한 예시적인 코드 +=+; 다른 이진 산술 연산자는 같은 방식으로 구현되어야합니다.

class X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};
inline X operator+(X lhs, const X& rhs)
{
  lhs += rhs;
  return lhs;
}

operator+=참조 당 결과를 반환하고 결과 operator+의 복사본 을 반환합니다. 물론 참조를 반환하는 것이 일반적으로 복사본을 반환하는 것보다 더 효율적이지만의 경우 operator+복사 주위에 방법이 없습니다. 을 쓰면 a + b결과가 새로운 값이되기를 기대하므로 새로운 값 operator+을 반환해야합니다. 3 또한 operator+왼쪽 피연산자 는 const 참조가 아닌 복사로 사용됩니다. 그 이유 operator=는 사본 당 인수 를 취하는 이유와 동일합니다 .

비트 조작 연산자 ~ & | ^ << >>는 산술 연산자와 같은 방식으로 구현되어야합니다. 그러나 (과부하 <<>>출력 및 입력 제외 ) 이들을 과도하게 사용하는 합리적인 사용 사례는 거의 없습니다.

이 즉에서 다시 공과 채취한다 a += b, 일반적으로,보다 효율적이다 a + b가능하면 바람직 할 것이다.

배열 첨자

배열 첨자 연산자는 이진 연산자이며 클래스 멤버로 구현해야합니다. 키로 데이터 요소에 액세스 할 수있는 컨테이너와 같은 유형에 사용됩니다. 이를 제공하는 정식 형태는 다음과 같습니다.

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

클래스 사용자가 반환 한 데이터 요소를 변경하지 못하게하려면 operator[](이 경우 비 const 변형을 생략 할 수 있음) 항상 연산자의 두 변형을 모두 제공해야합니다.

value_type이 내장 유형을 참조하는 것으로 알려진 경우 연산자의 const 변형은 const 참조 대신 복사본을 반환하는 것이 좋습니다.

class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const;
  // ...
};

포인터와 같은 유형의 연산자

자체 반복자 또는 스마트 포인터를 정의하려면 단항 접두사 역 참조 연산자 *와 이진 접두사 포인터 멤버 액세스 연산자 를 오버로드해야합니다 ->.

class my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

이것들도 거의 항상 const와 non-const 버전을 필요로합니다. 들어 ->오퍼레이터 경우 value_type이다 class(또는 struct또는 union다른)을 입력 operator->()until이 재귀 호출 operator->()이 아닌 클래스 타입의 리턴 값.

단항 주소 연산자에 과부하가 걸리지 않아야합니다.

들어하는 것은 operator->*()이 질문에 . 거의 사용되지 않으므로 과부하가 걸리지 않습니다. 실제로 반복자조차도 과부하되지 않습니다.


전환 연산자 계속


89
operator->()실제로 매우 이상합니다. 을 반환 할 필요가 없습니다 것 value_type*, 사실, 다른 클래스 유형을 반환 할 수 있습니다 - 클래스 유형이 가지고 제공operator->() 한 후 이후에 호출 될 것이다을. 이 재귀 호출은 리턴 유형이 발생할 operator->()때까지 진행 value_type*됩니다. 광기! :)
j_random_hacker

2
효과성에 관한 것이 아닙니다. 우리는 (매우) 몇 가지 경우에 전통적인 관용적 방식으로 그것을 할 수 없습니다. 결과를 계산하는 동안 두 피연산자의 정의를 그대로 유지해야합니다. 내가 말했듯이 행렬 곱셈과 다항식 곱셈의 두 가지 고전적인 예가 있습니다. 우리는 *용어로 정의 할 수 *=있지만 첫 번째 작업 중 하나가 *=새로운 객체, 계산 결과를 생성 하기 때문에 어색 할 것 입니다. 그런 다음 for-ijk 루프 후에이 임시 객체를로 바꿉니다 *this. 즉. 1.copy, 2.operator *, 3.swap
Luc Hermitte

6
`const value_type & operator * () const;`와 같은 포인터와 같은 연산자의 const / const 버전에 동의하지 않습니다. 이것은 dereferencing을 T* const반환하는 것과 같습니다 const T&. 즉, const 포인터는 const pointee를 의미하지 않습니다. 사실, 모방 T const *하는 것은 사소한 것이 아닙니다 const_iterator. 이것이 표준 라이브러리 의 모든 것들에 대한 이유입니다 . 결론 : 서명은 다음과 같아야합니다reference_type operator*() const; pointer_type operator->() const
Arne Mertz

6
한 의견 : 제안 된 이진 산술 연산자의 구현은 그렇게 효율적이지 않습니다. Boost 연산자 헤더 시뮬레이션 참고 : boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry 첫 번째 매개 변수의 로컬 사본을 사용하고 + =를 수행하고 로컬 사본. 이것은 NRVO 최적화를 가능하게합니다.
Manu343726

3
채팅에서 언급했듯이 대신 L <= R으로 표현할 수도 있습니다 . 최적화하기 어려운 표현으로 추가 인라인 레이어를 절약 할 수 있습니다 (또한 Boost.Operators가이를 구현하는 방법이기도 함). !(R < L)!(L > R)
TemplateRex

494

C ++에서 연산자 오버로드의 3 가지 기본 규칙

C ++에서 연산자 오버로드와 관련하여 따라야 할 세 가지 기본 규칙이 있습니다 . 이러한 모든 규칙과 마찬가지로 실제로 예외가 있습니다. 때때로 사람들은 그들로부터 벗어 났고 결과는 나쁜 코드가 아니었지만 그러한 긍정적 인 편차는 거의 없습니다. 적어도 내가 본 100 가지 중 99 가지의 편차는 정당화되지 않았습니다. 그러나 1000 개 중 999 개일 수도 있습니다. 따라서 다음 규칙을 따르는 것이 좋습니다.

  1. 운영자의 의미가 명백하고 명확하지 않을 때마다 과부하되지 않아야합니다. 대신, 잘 선택된 이름의 함수를 제공하십시오.
    기본적으로 과부하 연산자의 가장 중요한 규칙은 다음과 같이 말합니다. 하지 마십시오 . 연산자 오버로드에 대해 알려진 것이 많고 많은 기사, 서적 장 및 기타 텍스트가이 모든 것을 다루기 때문에 이상하게 보일 수 있습니다. 그러나이 명백한 증거에도 불구하고 , 운영자 과부하가 적절한 경우는 놀랍게도 거의 없습니다.. 그 이유는 애플리케이션 도메인에서 운영자의 사용이 잘 알려져 있고 논쟁의 여지가 없다면 운영자의 애플리케이션 뒤에 의미를 이해하기 어렵 기 때문입니다. 대중의 신념과는 달리 이것은 거의 사실이 아닙니다.

  2. 항상 운영자의 잘 알려진 의미를 고수하십시오.
    C ++는 오버로드 된 연산자의 의미에 제한이 없습니다. 컴파일러는 바이너리+연산자를구현하여올바른 피연산자를 빼는코드를 기꺼이 받아들입니다. 그러나, 운영자의 사용자는 표현 의심하지 않을 것a + ba에서를b. 물론 이것은 응용 프로그램 도메인에서 연산자의 의미론이 논란의 여지가 없다고 가정합니다.

  3. 항상 일련의 관련 작업을 모두 제공하십시오.
    운영자는 서로 및 다른 작업과 관련이 있습니다. 유형a + b이을지원하는경우 사용자a += b도전화를 걸 수있을 것으로 기대합니다. 접두사 증가를 지원하면 작동++a할 것으로 예상a++됩니다. 그들이 여부를 확인할 수 있다면a < b, 그들은 또한 여부를 확인할 수있을 것으로 기대합니다a > b. 그들이 당신의 유형을 복사 구성 할 수 있다면, 그들은 또한 과제가 잘 작동 할 것으로 기대합니다.


멤버와 비 멤버 사이의 결정으로 계속하십시오 .


16
내가 알고있는 유일한 것은 boost::spiritlol입니다.
Billy ONeal

66
@ 빌리 : 일부에 따르면, +문자열 연결에 대한 학대 는 위반이지만, 이제는 잘 확립 된 실천이되어 자연스럽게 보입니다. 나는 홈 브루 문자열 클래스를 기억하지만 90 년대 &에는이 목적 을 위해 바이너리 를 사용하는 것을 보았습니다 (기존의 실행을 위해 BASIC 참조). 그러나, 표준 라이브러리에 넣는 것은 기본적으로 이것을 돌로 설정했습니다. 학대 <<>>IO, BTW도 마찬가지 입니다. 왜 왼쪽-시프트가 명백한 출력 동작일까요? 우리가 첫 "Hello, world!"를 보았을 때 우리 모두가 그것에 대해 배웠기 때문입니다. 신청. 그리고 다른 이유는 없습니다.
sbi

5
@ curiousguy : 당신이 그것을 설명해야한다면, 분명히 명확하고 논쟁의 여지가 없습니다. 오버로드에 대해 논의하거나 방어해야하는 경우에도 마찬가지입니다.
sbi

5
@ sbi : "피어 리뷰"는 항상 좋은 생각입니다. 나에게 잘못 선택된 연산자는 잘못 선택된 함수 이름과 다르지 않습니다 (많은 것을 보았습니다). 연산자는 단지 기능입니다. 그 이상도 이하도 아닌. 규칙은 동일합니다. 아이디어가 좋은지 이해하려면 최선의 방법은 이해하는 데 시간이 얼마나 걸리는지 이해하는 것입니다. (따라서 동료 검토는 필수이지만 교리와 편견이없는 사람들 사이에서 동료를 선택해야합니다.)
Emilio Garavaglia

5
@ sbi 나에게 절대적으로 명백하고 논쟁의 여지가없는 사실은 operator==그것이 동등성 관계 여야한다는 것입니다 (IOW, 당신은 비 신호 NaN을 사용해서는 안됩니다). 컨테이너에는 많은 유용한 등가 관계가 있습니다. 평등이란 무엇입니까? " a동일 b수단" ab같은 수학적인 값을 가지고있다. (NaN이 아닌)의 수학적 가치 개념 float은 명확하지만, 컨테이너의 수학적 가치는 많은 고유 한 (유형의 재귀) 유용한 정의를 가질 수 있습니다. 평등에 대한 가장 강력한 정의는 "동일한 객체입니다"이며 쓸모가 없습니다.
curiousguy

265

C ++에서 연산자 오버로드의 일반적인 구문

C ++에서 내장 유형에 대한 연산자의 의미를 변경할 수 없으며 연산자는 사용자 정의 유형 1에 대해서만 오버로드 될 수 있습니다 . 즉, 피연산자 중 적어도 하나는 사용자 정의 유형이어야합니다. 다른 오버로드 된 기능과 마찬가지로 특정 매개 변수 집합에 대해 연산자를 한 번만 오버로드 할 수 있습니다.

모든 연산자가 C ++에서 오버로드 될 수있는 것은 아닙니다. 오버로드 할 수없는 연산자는 다음 . :: sizeof typeid .*과 같습니다. C ++의 유일한 삼항 연산자는?:

C ++에서 오버로드 될 수있는 연산자는 다음과 같습니다.

  • 산술 연산자 : + - * / %+= -= *= /= %=(모든 이진 접두사); + -(단항 접두사); ++ --(단항 접두사 및 접미사)
  • 비트 조작 : & | ^ << >>&= |= ^= <<= >>=(모든 이진 접두사); ~(단항 접두사)
  • 부울 대수 : == != < > <= >= || &&(모든 이진 접두사); !(단항 접두사)
  • 메모리 관리: new new[] delete delete[]
  • 암시 적 변환 연산자
  • 기타 : = [] -> ->* , (모든 이진 접두사); * &(모든 단항 접두사) ()(함수 호출, n- 항 접두사 )

그러나 이러한 모든 항목을 과부하 할 있다고 해서 그렇게 해야하는 것은 아닙니다 . 연산자 오버로드의 기본 규칙을 참조하십시오.

C ++에서 연산자는 특수한 이름을 가진 함수 형태로 오버로드됩니다 . 다른 함수와 마찬가지로 오버로드 된 연산자는 일반적으로 왼쪽 피연산자 유형의 멤버 함수 또는 멤버 함수 로 구현 될 수 있습니다 . 어느 쪽이든 자유롭게 사용할 수 있는지 또는 사용할 수 있는지 여부는 몇 가지 기준에 따라 다릅니다. 2 객체 x에 적용된 단항 연산자 @3operator@(x)또는로 호출 됩니다 x.operator@(). 및에 @적용되는 이진 접두사 연산자 를 또는로 호출합니다 . 4xyoperator@(x,y)x.operator@(y)

비 멤버 함수로 구현 된 연산자는 때때로 피연산자 유형과 친숙합니다.

1 "사용자 정의"라는 용어는 약간 잘못 될 수 있습니다. C ++은 내장 타입과 사용자 정의 타입을 구별합니다. 전자는 예를 들어 int, char 및 double에 속합니다. 후자는 표준 라이브러리의 타입을 포함하여 모든 구조체, 클래스, 공용체 및 열거 형에 속하지만 사용자가 정의하지는 않습니다.

2 이 내용은 이 FAQ 의 뒷부분 에서 다룹니다 .

3 @I가 자리로 사용할 이유에서 유효한 C ++ 연산자 아니다.

4 C ++의 유일한 삼항 연산자는 오버로드 될 수 없으며 유일한 n- 항 연산자는 항상 멤버 함수로 구현되어야합니다.


C ++에서 연산자 오버로드의 3 가지 기본 규칙을 계속하십시오 .


~이진 접두사가 아닌 단항 접두사입니다.
mrkj

1
.*오버로드 할 수없는 연산자 목록에서 누락되었습니다.
celticminstrel

1
@Mateen 나는 이것이 특별한 연산자가 아니라 모든 연산자에 적용된다는 것을 분명히하기 위해 실제 연산자 대신 자리 표시자를 사용하고 싶었 습니다. 그리고 C ++ 프로그래머가 되려면 작은 글씨에도주의를 기울여야합니다. :)
sbi

1
@HR :이 안내서를 읽었다면 무엇이 잘못되었는지 알 것입니다. 나는 일반적으로 질문과 관련된 첫 세 가지 답변을 읽어 보라고 제안합니다. 그것은 인생의 30 분 이상이되어서는 안되며, 기본적인 이해를 제공합니다. 나중에 찾을 수있는 연산자 별 구문입니다. 특정 문제는 operator+()멤버 함수 로 오버로드하려고 시도 하지만 무료 함수의 서명을주었습니다. 여기를 참조 하십시오 .
sbi

1
@ sbi : 나는 세 번째 게시물을 이미 읽었으며 작성해 주셔서 감사합니다. :) 나는 문제를 해결하려고 노력할 것입니다. 그렇지 않으면 별도의 질문으로 질문하는 것이 낫다고 생각합니다. 인생을 편하게 해주셔서 다시 한 번 감사드립니다! : D
Hosein Rahnama

251

회원과 비회원의 결정

이진 연산자 =(할당), [](배열 구독), ->(멤버 액세스) 및 n-ary ()(함수 호출) 연산자는 항상 언어 구문에 따라 멤버 함수 로 구현되어야합니다 .

다른 연산자는 멤버 또는 비 멤버로 구현할 수 있습니다. 그러나 일부는 왼쪽 피연산자를 수정할 수 없으므로 일반적으로 비 멤버 함수로 구현해야합니다. 이들 중 가장 눈에 띄는 것은 입력 및 출력 연산자 <<이며 >>, 왼쪽 피연산자는 표준 라이브러리의 스트림 클래스이며 변경할 수 없습니다.

멤버 함수 또는 비 멤버 함수로 구현하도록 선택해야하는 모든 연산자에 대해 다음 경험 규칙을 사용하여 결정하십시오.

  1. 단항 연산자 인 경우이를 멤버 함수 로 구현하십시오 .
  2. 이항 연산자가 두 피연산자를 모두 동일하게 취급하는 경우 (이를 변경하지 않은 채로 두는 경우)이 연산자를 멤버아닌 함수 로 구현하십시오 .
  3. 이항 연산자가 두 피연산자 모두를 동일하게 취급 하지 않으면 (보통 왼쪽 피연산자가 변경됨) 피연산자의 개인 부분에 액세스해야하는 경우 왼쪽 피연산자 유형의 멤버 함수 로 만드는 것이 유용 할 수 있습니다 .

물론 모든 경험 법칙과 마찬가지로 예외도 있습니다. 유형이 있다면

enum Month {Jan, Feb, ..., Nov, Dec}

그리고 증가 및 감소 연산자를 오버로드하려면 C ++에서 열거 형 유형에 멤버 함수를 사용할 수 없으므로 멤버 함수 로이 작업을 수행 할 수 없습니다. 따라서 무료 기능으로 과부하해야합니다. 또한 operator<()클래스 템플릿에 중첩 된 클래스 템플릿의 경우 클래스 정의에서 인라인 멤버 함수로 수행하면 훨씬 쉽게 읽고 쓸 수 있습니다. 그러나 이것들은 실제로 드문 예외입니다.

(그러나 예외를 만드는 경우const 멤버 함수의 this경우 암시적인 인수가 되는 피연산자 의 -ness 문제를 잊지 마십시오 . 멤버가 아닌 함수의 연산자가 가장 왼쪽 인수를 const참조 로 사용하는 경우 피연산자에 대한 -ness 문제를 잊지 마십시오 . , 멤버 함수와 동일한 연산자 const는 끝에 참조 가 있어야 *this합니다 const.)


오버로드 하려면 일반 연산자를 계속하십시오 .


9
Effective C ++의 Herb Sutter의 항목 (또는 C ++ 코딩 표준입니까?)은 클래스의 캡슐화를 높이기 위해 멤버가 아닌 비 친구 함수를 멤버 함수보다 선호해야한다고 말합니다. IMHO에서는 캡슐화 이유가 사용자의 경험치보다 우선하지만 경험치의 품질 값은 감소하지 않습니다.
paercebal

8
@paercebal : 효과적인 C ++ 는 Meyers, C ++ 코딩 표준 은 Sutter입니다. 어느 쪽을 언급하고 있습니까? 어쨌든 나는 operator+=()회원이 아니라는 생각을 싫어한다 . 왼쪽 피연산자를 변경해야하므로 정의에 따라 내부를 깊이 파고 들어야합니다. 회원이되지 않으면 무엇을 얻을 수 있습니까?
sbi

9
@sbi : C ++ 코딩 표준 (Sutter)의 항목 44 비회원 비 친구 함수 작성을 선호합니다 . 물론 클래스의 공용 인터페이스 만 사용하여이 함수를 실제로 작성할 수있는 경우에만 적용됩니다. 당신이 할 수없는 (또는 할 수는 있지만 성능을 저하시킬 수 있다면), 그것을 회원이나 친구로 만들어야합니다.
Matthieu M.

3
@ sbi : 죄송합니다, 효과적, 뛰어난 ... 내가 이름을 섞은 것도 당연합니다. 어쨌든 이득은 객체 개인 / 보호 데이터에 액세스 할 수있는 기능의 수를 가능한 한 많이 제한하는 것입니다. 이렇게하면 클래스의 캡슐화가 증가하여 유지 관리 / 테스트 / 진화가 쉬워집니다.
paercebal

12
@sbi : 하나의 예. operator +=및 클래스를 모두 사용하여 String 클래스를 코딩한다고 가정 해 보겠습니다 append. append내가 인덱스 N -1 인덱스에서 매개 변수의 문자열을 추가 할 수 있기 때문에 방법은 더 완료 : append(string, start, end)가지고 논리적 인 것 같습니다 +=과 통화 APPEND를 start = 0하고 end = string.size. 그 순간에 append는 멤버 메소드 일 수 있지만 멤버가 될 operator +=필요는 없으며 멤버가 아닌 멤버로 만들면 String 내부와 함께 연주하는 코드의 양이 줄어들므로 좋은 것입니다 .... ^ _ ^ ...
paercebal

165

전환 연산자 (사용자 정의 전환이라고도 함)

C ++에서는 컴파일러가 형식과 다른 정의 된 형식간에 변환 할 수 있도록하는 변환 연산자를 만들 수 있습니다. 암시 적 연산자와 명시 적 연산자의 두 가지 유형의 변환 연산자가 있습니다.

암시 적 변환 연산자 (C ++ 98 / C ++ 03 및 C ++ 11)

암시 적 변환 연산자 컴파일러 내재적 (사이의 변환과 같은 변환 할 수 intlong어떤 다른 유형의) 사용자 정의 형식의 값.

다음은 암시 적 변환 연산자가있는 간단한 클래스입니다.

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

하나의 인수 생성자와 같은 암시 적 변환 연산자는 사용자 정의 변환입니다. 컴파일러는 오버로드 된 함수에 대한 호출을 일치 시키려고 할 때 하나의 사용자 정의 변환을 허용합니다.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

처음에는 이것이 매우 도움이되는 것처럼 보이지만 이것의 문제점은 암시 적 변환이 예상되지 않은 경우에도 시작된다는 것입니다. 다음 코드에서는 lvalue 가 아니기 void f(const char*)때문에 호출되므로 첫 번째 코드가 일치하지 않습니다.my_string()

void f(my_string&);
void f(const char*);

f(my_string());

초보자도 쉽게이 문제를 경험할 수 있으며 심지어 숙련 된 C ++ 프로그래머도 컴파일러가 의심하지 않는 과부하를 선택하기 때문에 놀라게됩니다. 이러한 문제는 명시 적 변환 연산자로 완화 할 수 있습니다.

명시 적 변환 연산자 (C ++ 11)

암시 적 변환 연산자와 달리 명시 적 변환 연산자는 사용자가 원하지 않을 때 시작되지 않습니다. 다음은 명시 적 변환 연산자가있는 간단한 클래스입니다.

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

를 확인하십시오 explicit. 이제 암시 적 변환 연산자에서 예기치 않은 코드를 실행하려고하면 컴파일러 오류가 발생합니다.

prog.cpp : 'int main ()'함수에서 :
prog.cpp : 15 : 18 : 오류 : 'f (my_string)'호출을위한 일치하는 함수가 없습니다.
prog.cpp : 15 : 18 : 참고 : 후보자는 다음과 같습니다.
prog.cpp : 11 : 10 : 참고 : void f (my_string &)
prog.cpp : 11 : 10 : 참고 : 인수 1의 'my_string'에서 'my_string &'으로의 알려진 변환은 없습니다.
prog.cpp : 12 : 10 : 참고 : void f (const char *)
prog.cpp : 12 : 10 : 참고 : 인수 1의 'my_string'에서 'const char *'로의 알려진 변환은 없습니다.

명시 적 캐스트 연산자를 호출하려면 static_cast, C 스타일 캐스트 또는 생성자 스타일 캐스트 (예 :) 를 사용해야 T(value)합니다.

그러나 이에 대한 한 가지 예외가 있습니다. 컴파일러는 암시 적으로로 변환 할 수 bool있습니다. 또한 컴파일러는 변환 후 다른 암시 적 변환을 수행 할 수 없습니다 bool(컴파일러는 한 번에 2 개의 암시 적 변환을 수행 할 수 있지만 최대 1 개의 사용자 정의 변환 만 수행 할 수 있음).

컴파일러는 "과거"를 캐스팅하지 않기 때문에 bool이제 명시 적 변환 연산자는 Safe Bool 관용구 가 필요 하지 않습니다 . 예를 들어 C ++ 11 이전의 스마트 포인터는 Safe Bool 관용구를 사용하여 정수 유형으로 변환하지 못했습니다. C ++ 11에서 스마트 포인터는 대신 명시 적 연산자를 사용합니다. 컴파일러가 명시 적으로 형식을 bool로 변환 한 후 컴파일러가 암시 적으로 정수 형식으로 변환 할 수 없기 때문입니다.

오버로딩 new및으로delete 계속 진행하십시오 .


148

오버로드 newdelete

참고 : 이것은오버로드 구문 과을 처리new하며오버로드 된 연산자delete 구현 은 처리하지 않습니다. 나는 과부하의 의미론newdelete 운영자 과부하의 주제 내에서 자체 FAQ를받을 가치가 있다고 생각합니다.

기초

C ++에서이 편지 새로운 표현 처럼 new T(arg)첫째 : 두 가지이 식을 계산할 때 발생 operator new원시 메모리를 얻기 위해 호출 한 다음 적절한 생성자는 T유효한 객체로이 원시 메모리를 설정하는 호출됩니다. 마찬가지로 객체를 삭제하면 먼저 소멸자가 호출 된 다음 메모리가로 반환됩니다 operator delete.
C ++를 사용하면 메모리 관리 및 할당 된 메모리에서 개체의 구성 / 파괴와 같은 두 가지 작업을 모두 조정할 수 있습니다. 후자는 클래스의 생성자와 소멸자를 작성하여 수행됩니다. 미세 조정 메모리 관리는 자신의를 작성하여 수행 operator new하고 operator delete.

연산자 오버로딩의 첫 번째 기본 규칙 ( 하지 마십시오)은 특히 과부하 new및에 적용됩니다 delete. 이러한 연산자를 오버로드하는 거의 유일한 이유는 성능 문제메모리 제약 이며, 많은 경우에 사용 된 알고리즘 변경 과 같은 다른 작업 은 메모리 관리를 조정 하는 것 보다 훨씬 높은 비용 / 이득 비율을 제공합니다 .

C ++ 표준 라이브러리는 미리 정의의 세트와 함께 제공 newdelete운영. 가장 중요한 것은 다음과 같습니다.

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

처음 2 개는 객체에 메모리를 할당 / 할당하고, 2 개는 객체 배열에 할당합니다. 고유 한 버전을 제공하면 과부하되지 않지만 표준 라이브러리 의 버전을 대체 합니다.
당신이 과부하되면 operator new, 당신은 항상 또한 일치 과부하해야 operator delete당신이 그것을 호출 할 의도가 없다하더라도. 새 표현식을 평가하는 동안 생성자가 발생하면 런타임 시스템은 메모리 를 할당하기 위해 호출 된 것과 operator delete일치하는 메모리를 반환 operator new하여 오브젝트를 작성하기 때문입니다. operator delete기본 이름이 호출되는데 이는 거의 항상 잘못된 것입니다.
당신이 과부하 경우 newdelete, 당신도, 변형 배열 과부하를 고려해야한다.

놓기 new

C ++를 사용하면 new 및 delete 연산자가 추가 인수를 취할 수 있습니다.
소위 게재 위치 신규를 사용하면 특정 주소에 객체를 생성 할 수 있습니다.

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

표준 라이브러리는이를 위해 new 및 delete 연산자의 적절한 과부하를 제공합니다.

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

operator deleteX의 생성자가 예외를 throw하지 않는 한 위에 주어진 new 배치에 대한 예제 코드에서는 호출되지 않습니다.

또한 과부하 수 newdelete다른 인수. 새로운 게재 위치에 대한 추가 인수와 마찬가지로 이러한 인수는 키워드 뒤에 괄호 안에 표시됩니다 new. 역사적 이유로, 이러한 변형은 인수가 특정 주소에 객체를 배치하기위한 것이 아니더라도 종종 새로운 배치라고도합니다.

클래스 별 신규 및 삭제

측정 결과 특정 클래스 또는 관련 클래스 그룹의 인스턴스가 자주 생성 및 소멸되고 런타임 시스템의 기본 메모리 관리가 일반적인 성능은이 특정 사례에서 비효율적으로 처리됩니다. 이를 개선하기 위해 특정 클래스에 대해 새로운 과부하 및 삭제를 수행 할 수 있습니다.

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

따라서 과부하가 걸리면 new 및 delete는 정적 멤버 함수처럼 작동합니다. 의 객체의 my_class경우 std::size_t인수는 항상입니다 sizeof(my_class). 그러나 이러한 연산자는 파생 클래스 의 동적으로 할당 된 객체에 대해서도 호출되며 ,이 경우 이보다 클 수 있습니다.

글로벌 신규 및 삭제

새로운 전역 과부하 및 삭제를 수행하려면 표준 라이브러리의 사전 정의 된 연산자를 자체 연산자로 바꾸십시오. 그러나 이것은 거의 수행 할 필요가 없습니다.


11
또한 전역 연산자 new 및 delete를 대체하는 것이 일반적으로 성능을위한 것임을 동의하지 않습니다. 반면, 일반적으로 버그 추적입니다.
Yttrill

1
오버로드 된 새 연산자를 사용하는 경우 일치하는 인수를 가진 삭제 연산자도 제공해야합니다. 글로벌 신규 / 삭제 섹션에서 그다지 관심이없는 부분을 말합니다.
Yttrill

13
@Yttrill 당신은 혼란스러운 것들입니다. 의미는 오버로드됩니다. "연산자 과부하"의 의미는 의미에 과부하가 걸린다는 것입니다. 그것은 말 그대로 기능에 과부하가 걸린다는 것을 의미하지 않으며, 특히 new 연산자는 표준 버전에 과부하를주지 않습니다. @sbi는 반대 주장하지 않습니다. "오버로딩 추가 연산자"라고하는 것과 같이 "오버로딩 새"라고 부르는 것이 일반적입니다.
Johannes Schaub-litb

1
@sbi : 참조 (또는 더 나은, 링크) gotw.ca/publications/mill15.htm . 때로는 nothrow새로운 것을 사용하는 사람들에게만 좋은 습관 입니다.
Alexandre C.

1
"일치하는 연산자 삭제를 제공하지 않으면 기본 인수가 호출됩니다."-> 실제로, 인수를 추가하고 일치하는 삭제를 작성하지 않으면 연산자 삭제가 전혀 호출되지 않으며 메모리 누수가 발생합니다. (15.2.2, 객체가 차지하는 스토리지는 적절한 ... 연산자 삭제가 발견 된 경우에만 할당 해제됩니다)
dascandy

46

operator<<객체를 std::cout파일로 스트리밍 하거나 파일로 스트리밍하는 기능을 멤버 함수로 사용할 수없는 이유는 무엇 입니까?

가지고 있다고 가정 해 봅시다.

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

주어진 경우 다음을 사용할 수 없습니다.

Foo f = {10, 20.0};
std::cout << f;

이후 operator<<의 멤버 함수로서 과부하 Foo, 운전자의 LHS는 있어야 Foo개체. 즉, 다음을 사용해야합니다.

Foo f = {10, 20.0};
f << std::cout

매우 직관적이지 않습니다.

비 멤버 함수로 정의하면

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

다음을 사용할 수 있습니다.

Foo f = {10, 20.0};
std::cout << f;

매우 직관적입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.