프로그래밍 언어/C++

[C++] 연산자 오버로딩(Overloading)

ssung.k 2019. 10. 3. 17:53

오버로딩이란?

오버로딩(Overloading)은 메서드의 이름은 하나만 주고 매개변수(parameter)를 다르게 함으로써 메서드를 여러개 만드는 것을 말합니다.

예를 들어볼까요? 정수와 실수에 대해서 덧셈을 하는 두 함수를 정의해보았습니다.

int intSum (int a,int b){
  return a+b;
}

double doubleSum (double a, double b){
  return a+b;
}

지금은 함수가 두 개라서 문제없어 보이지만 여러 자료형에 대해서 다루고 싶은 경우에는 각각 함수를 새로 만들어야 합니다. 함수의 내부는 같은데 계속 이름도 지어야하고, 이만저만 불편합니다. 그래서 나온 개념이 바로 오버로딩 입니다. 여러 개의 이름을 쓸 필요없이 같은 이름을 사용할 수 있는 것이죠.

int sum (int a,int b){
  return a+b;
}

double sum (double a, double b){
  return a+b;
}

같은 이름을 가진 여러 함수 중에 어떤 함수를 사용해야 할지는 매개변수를 통해 구분을 합니다. 매개변수의 개수가 같고, 같은 자료형을 받고 있는 함수를 찾아서 돌아가는 것이죠. 이 귀찮은 작업을 바로 컴파일러가 처리해줍니다.

컴파일러가 매개변수의 개수와 자료형을 확인한 후 다른 함수로 바꿔서 사용하게 되는 것이죠.

 

연산자 오버로딩

그렇다면 연산자 오버로딩이란 무엇일까요?

연산자 오버로딩은 C++ 에서 제공하는 기본 타입이 아닌 클래스 타입, 즉 사용자 정의 타입에도 연산자를 사용할 수 있게 하는 문법입니다. 다시 말해서, 오버로딩이 같은 이름의 함수를 통해 여러 기능을 수행하였다면, 이번에는 같은 이름을 가진 연산자들이 상황에 맞추어 여러 기능을 수행한다는 의미입니다.

예시를 통해 살펴봅시다. 우선 Edge 라는 클래스를 정의해주었습니다. 필드값으로는 해당 엣지가 연결하고 있는 양 쪽 노드와 엣지의 거리 distance 가 있습니다.

class Edge {
public:
    int node[2];
    int distance;
    
    Edge(int a,int b,int distance){
        this->node[0] = a;
        this->node[1] = b;
        this->distance = distance;
    }
};

이제 두 개의 Edge 를 만든 후, 이를 더하면 어떻게 될까요?

Edge Edge1 = Edge(1,7,12);
Edge Edge2 = Edge(1,4,28);
    
cout << Edge1 + Edge2 << "\n";

당연히 오류가 발생합니다. 컴파일러는 두 객체의 연산을 알고 있지 않기 때문입니다. 여기서 더하라는게 node 를 더하라는건지, distance 를 더하라는건지, 둘 다 더하라는건지 어디에도 정의되어 있지 않기 때문이죠.

따라서 이를 정의해주어야 합니다.

방법 1

class Edge {
public:
    int node[2];
    int distance;
    
    Edge(int a,int b,int distance){
        this->node[0] = a;
        this->node[1] = b;
        this->distance = distance;
    }
  
  	int operator+(Edge &edge){
      	return this->distance + edge.distance;
    }
};

 

이제 우리가 선언한 Edge 라는 클래스 타입의 객체에 연산자를 사용하면 컴파일러는 기존의 연산자가 아닌 정의된 함수를 호출하게 됩니다.

Edge e1 = Edge(1,7,12);
Edge e2 = Edge(1,4,28);
    
cout << e1 + e2 << "\n"; // e1.operator+(e2)

해당 연산은 주석친 부분으로 바뀌게 되어 다음과 같은 의미를 가집니다.

e1 객체를 기준으로 메소드 operator+ 를 호출하고 매개변수로 e2 를 받는다.

따라서 우리가 원하는 연산이 가능해지는 것이죠.

 

방법 2

비슷하지만 약간 다른 방법도 존재합니다.

class Edge {
public:
    int node[2];
    int distance;
    
    Edge(int a,int b,int distance){
        this->node[0] = a;
        this->node[1] = b;
        this->distance = distance;
    }
  
  	int operator+(Edge &edge);	
};

int Edge::operator+(Edge &edge){
    return this->distance + edge.distance;
}

클래스 내에서는 해당 메소드를 선언만 해주고 밖에서 정의해주는 차이가 있을 뿐 동일한 형태입니다. 단 밖에다가 메소드를 사용할 때는 Edge 클래스의 메소드라고 Edge:: 다음과 같이 명시해주어야 합니다.

 

방법 3

class Edge {
public:
    int node[2];
    int distance;
    
    Edge(int a,int b,int distance){
        this->node[0] = a;
        this->node[1] = b;
        this->distance = distance;
    }
};

int operator+(Edge &edge1, Edge &edge2){
    return edge1.distance + edge2.distance;
}

이번에는 클래스의 메소드가 아닌 global 한 operator 입니다. 이럴 경우에는 컴파일러 입장에서도 다른 해석을 하게 됩니다.

Edge e1 = Edge(1,7,12);
Edge e2 = Edge(1,4,28);
    
cout << e1 + e2 << "\n"; // operator+(e1,e2)

주석을 보면 위와의 차이를 알 수 있을 겁니다. 그렇기 때문에 받는 매개변수도 바뀌어야 합니다.

 

사용자 정의 객체 정렬

그렇다면 우리가 정의한 클래스의 객체들을 정렬하고 싶다면 어떻게 해야할까요?

class Edge {
public:
    int node[2];
    int distance;
    
    Edge(int a,int b,int distance){
        this->node[0] = a;
        this->node[1] = b;
        this->distance = distance;
    }
};

int main(){
  	vector <Edge> v;
  
    v.push_back(Edge(1,7,12));
    v.push_back(Edge(1,4,28));
    v.push_back(Edge(1,2,67));
    v.push_back(Edge(1,5,17));
    v.push_back(Edge(2,4,24));
    v.push_back(Edge(2,5,62));
    v.push_back(Edge(3,5,20));
    v.push_back(Edge(3,6,37));
    v.push_back(Edge(4,7,13));
    v.push_back(Edge(5,6,45));
    v.push_back(Edge(5,7,73));

    sort(v.begin(), v.end());
}

vector 에 여러 Edge 를 넣고 이를 distance 에 대해서 정렬을 하려고 합니다. 그러기 위해서는 < 를 연산자 오버로딩을 해야겠죠?

하지만 위와 같은 방식으로 정의한다면 오류나 납니다. 오류를 따라가보니 algorithm 헤더까지 올라가고, 그 안에 다음과 같은 부분에서 오류가 나게 됩니다.

template <class _T1>
struct __less<_T1, _T1>
{
    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX11
    bool operator()(const _T1& __x, const _T1& __y) const {return __x < __y;}
};

 

정확히 무엇인지 잘 모르겠어도 눈에 띄는 것은 매개변수들을 모두 const 로 받습니다. 그렇기 때문에 연사자 오버로딩을 할 때도 이 부분을 추가해줘야 합니다.

class Edge {
public:
    int node[2];
    int distance;
    
    Edge(int a,int b,int distance){
        this->node[0] = a;
        this->node[1] = b;
        this->distance = distance;
    }
  
  	bool operator<(const Edge &edge) const {
      	return (this->distance) < (edge.distance);
    }
};

operator 라는 메소드 뒤에 const 를 명시하게 되면 operator 라는 메소드 안에서는 어떤 변수도 바꿀 수 없고, const 가 아닌 메소드를 부를 수도 없게 됩니다.