본문 바로가기

진리는어디에/C++

[C++] 연산자 오버로딩(operator overloading) 완벽 가이드

들어가며

C++은 사용자 정의 타입의 연산을 정의하는 연산자의 동작을 재정의 할 수 있는 연산자 오버로딩(operator overloading)을 제공한다. 이번 포스트는 연산자 오버로딩에 대해 자세히 살펴 보도록한다.

연산자 자체에 대한 보다 자세한 사항은 [여기]를 살펴 보도록 한다.

연산자 오버로딩을 설명하기 위해 아래 Point 클래스를 살펴 보자. Point 클래스는 int x, y를 멤버로 가지고, display 함수가 호출되면 x와 y 값을 출력 해주는 간단한 클래스다.

class Point
{
    int x;
    int y;
public:
    Point(int x, int y) : x(x), y(y) {}

    void display() const 
    {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }
};

Point 클래스의 인스턴스는 아래 처럼 간단히 선언할 수 있다.

Point p1(1, 1);
Point p2(2, 2);

위 코드는 아무런 문제 없이 컴파일 된다. 이제 p1과 p2의 값을 더한 p3라는 인스턴스를 만들어 보자.

Point p3 = p1 + p2;

p1과 p2를 더한다는 의미로 위와 같이 두 객체에 대해 '+' 연산자를 적용 했다. 당연하지만 위 코드는 컴파일 에러를 발생 시킨다.

C++ 컴파일러는 int와 같은 기본 타입의 + 연산은 어떻게 동작해야 하는지 알고 있지만, Point 클래스 처럼 사용자 정의 타입의 + 연산자는 어떻게 동작해야 하는지 정의 되어 있지 않기 때문에 컴파일 에러를 발생 시킨다.

C++은 이런 사용자 정의 타입의 연산자에 대해 컴파일러가 이해할 수 있도록 연산자를 재정의 할 수 있는 기능을 제공하고 있고 이것을 연산자 오버로딩이라고 한다.

연산자 오버로딩 구현

방금 C++은 클래스와 구조체 같은 사용자 정의 타입에 대해 연산자를 재정의 할 수 있다고 말했다. 재정의 방법은 연산자의 동작을 정의하는 함수를 클래스 멤버 함수 또는 전역 함수로 추가하면 된다.

이번 섹션에서는 연산자 오버로딩을 구현하는 두 방식의 구현 방법과 차이에 대해 살펴 보도록 하겠다.

전역 함수

아래와 같이 클래스 외부에 Point operator + (const Point& p1, const Point& p2) 라는 전역 연산자 오버로딩 함수를 작성했다.

class Point
{
    int x;
    int y;
public:
    Point(int x, int y) : x(x), y(y) {}

    void display() const 
    {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }
};

Point operator+(const Point& p1, const Point& p2)
{
    return Point(p1.x + p2.x, p1.y + p2.y);
}

Point operator + (const Point& p1, const Point& p2)는 Point 타입에 대한 + 연산이 호출 되는 경우, 이 함수를 적용해 달라고 컴파일러에게 알려주는 것이다. 이제 p1 + p2를 호출하게 되면 컴파일러가 똑똑하게 위의 operator + 함수를 호출 해줄 것이다...는 개뿔...위 코드는 여전히 컴파일 에러를 발생 시킨다.

Point 클래스의 x, y 멤버 변수는 private 접근 권한을 가지고 있기 때문에 전역 함수에서 x와 y에 접근할 수 없다는 에러를 발생 시킨다. 해결 방법으로는 :

  • 두 변수를 public으로 접근 권한 변경
  • public으로 선언된 get/set 멤버 함수를 추가

정도를 생각해 볼 수 있지만 모두 아름다운 형태는 아니다.

그래서 일반적으로 전역 함수를 이용해 오퍼레이터 오버로딩을 구현하면 private 멤버에도 접근할 수 있도록 friend 속성을 걸어 주는것이 관용적인 사용법이다.

class Point
{
    int x;
    int y;
public:
    Point(int x, int y) : x(x), y(y) {}

    void display() const 
    {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }

    friend Point operator+(const Point& p1, const Point& p2);
};

Point operator+(const Point& p1, const Point& p2)
{
    return Point(p1.x + p2.x, p1.y + p2.y);
}

int main()
{
    Point p1(1, 1);
    Point p2(2, 2);
    Point p3 = p1 + p2;
    p3.display();
    return 0;
}

멤버 함수

이번 섹션에서는 멤버 함수를 이용해 연산자 오버로딩을 구현해 보고 전역 함수와는 무슨 차이가 있는지 살펴 보도록 하겠다.

class Point
{
    int x;
    int y;
public:
    Point(int x, int y) : x(x), y(y) {}

    void display() const 
    {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }
	
    Point operator + (const Point& other) const
    {
        return Point(x + other.x, y + other.y);
    }
};

위 코드에서 가장 먼저 눈에 띄는 것은 오퍼레이터 오버로딩 함수의 인자 갯수다. + 연산자는 이항  연산이기 때문에 전역 함수 버전에서는 두 개의 인자를 받았지만 멤버 함수는 하나의 인자만 받고 있다. 이는 전역 함수와 멤버 함수의 오버로딩 된 함수 호출 방식의 차이 때문이다.

아래는 사용자가 보기에는 동일한 코드지만 컴파일러는 다르게 분석 한다.

Point p3 = p1 + p2;

// 전역 함수인 경우 컴파일러가 이해하는 코드
Point p3 = operator + (p1, p2);

// 멤버 함수인 경우 컴파일러가 이해하는 코드
Point p3 = p1.operator + (p2);

멤버 함수로 오퍼레이터 오버로딩을 구현하는 경우, 자기 자신이 연산에 포함되므로 오퍼레이터 함수 인자는 연산에 필요한 항 갯수 보다 하나 더 적게 필요하다.

만일 이 둘을 동시에 만든다면 어떻게 될까? 일단 멤버 함수와 전역 함수 둘 다 구현한다고 해도 컴파일 오류 같은 현상은 발생하지 않는다. 다만 멤버 함수가 높은 우선 순위를 가지고 있기 때문에 컴파일러는 멤버 함수 버전의 오퍼레이터 오버로딩 함수를 먼저 호출한다.

단, operator + (p1, p2) 와 같이 전역 오퍼레이터 함수를 명시적으로 호출하면 우선 순위와 관계 없이 호출 가능하다. 반대로 p1.operator + (p2)와 같이 멤버 함수 버전 오퍼레이터 함수도 명시적으로 호출하는 것 또한 가능하다.

주의 사항

아래는 오퍼레이터 오버로딩 시 주의 사항들을 나열 했다.

  • 오퍼레이터 오버로딩 함수의 모든 인자가 기본(primitive) 타입인 경우 오버로딩 불가능
  • 아래 연산자들은 오버로딩 불가능 :
    • ., *, ::, ?:, sizeof, typeid, static_cast, dynamic_cast, reinterpret_cast, const_cast
      . 는 C++20 부터 오버로딩 가능
  • 다음 연산자들은 멤버 함수로만 오버로딩 가능 : = () [] ->
  • 새로운 연산자는 만들 수없다. 예를 들어 [)와 같이 C++문법에 정의되지 않은 연산자는 재정의 불가능
  • 연산자 우선 순위를 변경할 수 없다. 사칙 연산에서 +, - 가 *, / 보다 높은 순서를 가질 수 없다.
  • 연산자의 항 갯수를 변경할 수 없다. 이항 연산인 +를 단항 연산으로 변경할 수 없다.
  • 연산자 오버로딩 함수는 디폴트 인자를 지정할 수 없다.

마치며

...

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!