c++ - 예제 - 연산자 오버로딩의 기본 규칙과 관용구는 무엇입니까?




operator overloading in c++ (5)

참고 : 응답은 특정 순서 로 주어졌지만, 많은 사용자가 주어진 시간이 아닌 투표에 따라 답변을 정렬하기 때문에 가장 이해하기 쉬운 순서로 대답 색인을 작성합니다 .

(참고 :이 항목은 Stack Overflow의 C ++ FAQ에 대한 항목입니다.이 양식에 FAQ를 제공한다는 아이디어를 비판하고 싶다면이 모든 것을 시작한 메타에 게시 하면됩니다. 그 질문은 C ++ 대화방 에서 모니터링됩니다. FAQ 은 처음에 FAQ 아이디어가 시작 되었기 때문에 아이디어가 떠오른 사람들이 대답을 읽을 가능성이 높습니다.)


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

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

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

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

  3. 항상 관련된 모든 작업을 제공하십시오.
    운영자는 서로 및 다른 작업과 관련이 있습니다. 유형 a + b 지원 a + b 사용자는 a += b 도 호출 할 수 있습니다. 접두사 증분 ++a 지원 a++ 가 잘 작동 할 것으로 기대됩니다. 그들이 a < b 인지 여부를 확인할 수 있다면, a > b 인지 여부를 확인할 수 있어야합니다. 사용자가 유형을 복사 할 수있는 경우 할당이 올바르게 작동 할 것으로 예상됩니다.

회원과 비회원 간의 결정을 계속하십시오.


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

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

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

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

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

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

C ++에서 연산자는 특수한 이름을 가진 함수 형태로 오버로드됩니다. 다른 함수와 마찬가지로 오버로드 된 연산자는 일반적으로 왼쪽 피연산자 유형의 멤버 함수 또는 멤버 함수 로 구현 될 수 있습니다. 선택을 자유롭게하거나 둘 중 하나를 사용할 것인지 여부는 몇 가지 기준에 따라 다릅니다. 2 객체 x에 적용된 단항 연산자 @ 3[email protected](x) 또는 [email protected]() 로 호출됩니다. 객체 xy 적용된 이진 중온 연산자 @[email protected](x,y) 또는 [email protected](y)[email protected](y) 됩니다. 4

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

1 "사용자 정의"라는 용어는 약간 혼동을 줄 수 있습니다. C ++은 기본 제공 형식과 사용자 정의 형식을 구분합니다. 예를 들어, int는 char, double을,; 후자의 경우, 표준 라이브러리의 구조체, 클래스, 공용체 및 열거 형은 사용자가 정의하지 않더라도 표준 라이브러리의 구조체, 클래스, 공용체 및 열거 형에 속합니다.

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

3 @ 는 C ++에서 유효한 연산자가 아니기 때문에 이것을 자리 표시 자로 사용합니다.

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

C ++에서 연산자 오버로딩의 세 가지 기본 규칙으로 계속 진행하십시오.


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

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*() )

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

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: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

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

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

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

계속 newdelete 계속합니다.


new 과부하 및 delete

참고 : 이 연산자는 오버로드 된 연산자의 구현 이 아니라 newdelete 의 오버로드 구문 에만 적용됩니다. 나는 연산자 오버로딩의 주제 내에서 new 를 오버로딩하는 의미 와 자신의 FAQ를 가질 가치가 있다고 생각한다.

기초

C ++에서 new T(arg) 와 같은 새로운 표현식 을 작성할 때이 표현식을 평가할 때 두 가지 일이 발생합니다. 첫 번째 operator new 가 원시 메모리를 얻기 위해 호출 된 다음 T 의 적절한 생성자가 호출되어이 원시 메모리를 유효한 객체. 마찬가지로 객체를 삭제하면 먼저 소멸자가 호출 된 다음 메모리가 operator delete 로 반환됩니다.
C ++에서는 메모리 관리와 할당 된 메모리에서 객체의 생성 / 삭제와 같은 두 가지 작업을 모두 조정할 수 있습니다. 후자는 클래스의 생성자와 소멸자를 작성하여 수행됩니다. 미세 조정 메모리 관리는 operator new operator deleteoperator delete 를 작성하여 수행됩니다.

연산자 오버로딩의 기본 규칙 중 첫 번째는 수행하지 않습니다 . 특히 new 오버로딩과 delete 적용됩니다. 이러한 연산자를 과부하하는 유일한 이유는 성능 문제메모리 제약 이며, 대부분의 경우 알고리즘 사용 변경 과 같은 다른 동작은 메모리 관리를 조정하는 것 보다 훨씬 높은 비용 / 이득 비율을 제공합니다.

C ++ 표준 라이브러리에는 미리 정의 된 new 연산자 및 delete 연산자가 있습니다. 가장 중요한 것들은 다음과 같습니다 :

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(); 

첫 번째 두 개는 객체에 대한 메모리를 할당 / 할당 해제하고, 두 개는 객체 배열에 대해 메모리를 할당 / 할당 해제합니다. 자신의 버전을 제공하면 과부하 가 발생 하지 않고 표준 라이브러리의 버전으로 대체 됩니다.
operator new 를 오버로드하면 절대 호출하지 않으려는 경우에도 일치하는 operator delete 항상 오버로드해야합니다. 그 이유는 생성자가 새로운 표현식을 평가하는 동안 throw 될 경우 런타임 시스템은 객체를 생성하기 위해 메모리를 할당하기 위해 호출 된 operator new 일치하는 operator delete 메모리를 반환합니다. 일치하는 operator delete 제공하지 않으면, 기본값이 호출됩니다. 이는 거의 항상 잘못된 것입니다.
new 오버로드하고 delete 하는 경우 배열 변형도 오버로드하는 것을 고려해야합니다.

new 게재 위치

C ++은 새로운 연산자와 delete 연산자가 추가적인 인수를 취하도록합니다.
소위 placement new를 사용하면 다음과 같이 전달되는 특정 주소에 객체를 만들 수 있습니다.

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(); 

X의 생성자가 예외를 throw하지 않는 한 위에 명시된 배치 배치 코드 예제에서 operator delete 는 호출되지 않습니다.

new 를 오버로드하고 다른 인수로 delete 수도 있습니다. placement new에 대한 추가 인수와 마찬가지로이 인수는 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) 됩니다.그러나 이러한 연산자는 파생 클래스 의 동적으로 할당 된 개체에 대해서도 호출되며 ,이 경우에는 그보다 클 수 있습니다.

글로벌 신규 및 삭제

글로벌 전역을 오버로드하고 삭제하려면 단순히 표준 라이브러리의 사전 정의 된 연산자를 자체 라이브러리로 대체하십시오. 그러나 이것은 거의 수행 할 필요가 없습니다.


왜 파일 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;

매우 직관적입니다.





c++-faq