C++ operator

3 분 소요

연산자 오버로딩

객체에 대해 +, - 등의 연산을 수행할 때가 있다. 객체끼리 더하거나, 파일에 객체를 스트림으로 전달하거나, 가져올 수도 있다. 클래스에서 덧셈을 구현하는 방법은 크게 세가지 방법이 있을 것이다. 두개를 먼제 비교하자면, 하나는 클래스 내부에 메서드로 구현하는 방법과 다른 하나는 연산자 오버로딩을 통해서 구현할 수 있다. 메서드로 구현하자면
#include <iostream>

template <typename T> class MyClass
{
  private:
    T _data;

  public:
    MyClass(T data) : _data(data)
    {
    }
    T getMyData() const
    {
        return _data;
    }
    MyClass add(const MyClass &object) const
    {
        return MyClass<T>(getMyData() + object.getMyData());
    }
};
int main(int argc, char *argv[])
{
    MyClass<int> first{1};
    MyClass<int> second{2};

    MyClass<int> third = first.add(second);
    return 0;
}
두 클래스를 더해서 그 결과를 새로 생성한 클래스에 담아서 리턴한다. 원본을 변경하지 않도록 const 키워드를 추가하고 MyClass 에 대한 레퍼런스로 받도록 선언한다. 작동에는 문제가 없지만 지저분하고 개선의 여지가 있다. 그럼 연산자 오버로딩을 봐보자!
#include <iostream>

template <typename T> class MyClass
{
  private:
    T _data;

  public:
    MyClass(T data) : _data(data)
    {
    }
    T getData() const
    {
        return _data;
    }
    MyClass operator+(const MyClass &object) const
    {
        return MyClass(getData() + object.getData());
    }
};
int main(int argc, char *argv[])
{
    MyClass<int> first{1};
    MyClass<int> second{5};

    MyClass<int> third = first + second;
    return 0;
}
C++는 덧셈 기호를 클래스 안에서 원하는 형태로 정의하는 덧셈 연산자 기능을 지원한다. 구현 방법은 위에서 보는 것처럼 operator+ 란 메서드로 정의한다. 두 셀을 더할때 덧셈 기호를 쓸 수 있게 된다.
// ...
MyClass<int> third = first + second;
// ...
위 코드를 다음과 같이 변환해준다.
// ...
MyClass<int> third = first.operator+(second);
// ...
이 방식이 익숙해지려면 시간이 필요하다. operator+ 라는 이름이 어색하다. 정확히 이해하려면 내부 작동과정을 살펴볼 필요가 있다. C++ 컴파일러가 프로그램을 파싱할 때 +, -, =, <<와 같은 연산자를 발견하면 여기 나온 것과 매개변수가 일치하는 operator+, operator-, operator=, operator<<라는 이름의 함수나 메서드가 있는지 확인한다.

여기서 operator+ 매개변수가 반드시 이 메서드가 속한 클래스와 같은 타입의 객채만 받을 필요는 없다. 해당 객체를 받아서 사용하도록 하면 된다. 그리고 operator+ 의 리턴 타입도 마음껏 정할 수 있다. 함수 오버로딩을 떠올려보면 함수의 리턴 타입을 따지지 않았다. 연산자 오버로딩도 일종의 함수 오버로딩이다. 묵시적 변환과 explicit 위 예제처럼 operator+를 정의하면 클래스 끼리 더할 수 있을 뿐 아니라 string_view, double, int와 같은 값도 더할 수 있다. 묵시적 변환 때문인데,,, 이렇게 할 수 있는 이유는 컴파일러가 단순히 operator+만 찾는 데 그치지 않고 타입을 정확히 변환할 수 있는 방법도 찾기 때문이다. 또한 지정된 타입을 변환할 방법도 찾는다. 생성자는 이렇게 타입을 변환하는 역할을 하기에 적합하다.
MyClass first{1}, second{2};
std::string str = "hello";
MyClass third = first + second;
third = first + 0.5;
third = second + 10;
만약 컴파일러가 자신에 double 값을 더하는 MyClass를 발견하면 먼저 double 타입의 인수를 받는 MyClass생성자를 찾아서 임시 객체를 생성한 뒤 operator+로 전달한다. spring_view, int 도 마찬가지다.

이렇게 묵시적 변환을 활용하면 편리할 때가 있다. 하지만 string_view를 더하는 것은 상식적으로 맞지가 않다. 묵시적으로 변환하지 않으려면 explicit 키워드를 추가한다.
class MyClass
{
    public:
        MyClass() = default;
        MyClass(double data);
        explicit MyClass(std::string_view data);
};
  • explicit 키워드는 클래스를 정의하는 코드에서만 지정할 수 있다.
  • 인수를 하나만 지정해서호출할 수 있는 생성자에만 적합하다. 매개변수가 하나뿐인 생성자나 매개변수를 여러 개 받더라도 디폴드값이 지정된 생성자에만 붙일 수 있다.
  • 묵시적 변환을 위해 성성자를 선택하는 과정에서 성능이 떨어질 수 있다. 항상 임시 객체를 생성하기 때문이다. double 값을 더할 때 이렇게 임시 객체가 생성되지 않게 하려면 추가적으로 operator+를 함꼐 정의한다.

전역 함수로 구현하기

세번째 방법으로, 전역 함수로 구현하는 방법이 있다. 지금까지 좌변에 operator를 오버딩한 객체만 두고 코드를 봤는데 이유는 C++가 교환법칙이 성립되지 않기 떄문이다. 왜냐면 operator는 반드시 객체에 대해서 호출해야 하고 이를 위해 객체가 항상 operator의 좌변에 위치해야 하기 때문이다. 하지만 전역 함수로 만들면 특정 객체에 종속되지 않기 때문에 가능하게 할 수 있다.
#include <iostream>

class MyClass
{
  private:
    double _data;

  public:
    MyClass() = default;
    MyClass(double data): _data(data) {};
    double getData() const {return _data; }
    void printData() {std::cout << "data: " << _data << std::endl; }
};

MyClass operator+(const MyClass& first, const MyClass& second) {
    return MyClass(first.getData() + second.getData());
}

int main(int argc, char *argv[])
{
    MyClass first{1}, second{2};
    MyClass third = first + 3;
    third.printData();
    third = 4 + 3.0;
    third.printData();
    return 0;
}
위 코드대로 컴파일 하면 문제가 없다.
third = 4 + 3;
이 코드를 보면 아래 코드로 변환이된다.
third = 7;
컴파일러는 MyClass 클래스에서 explicit이 지정되지 않았으면서 double 타입인수를 받는 사용자 정의 생성자를 찾아서 double 값을 임시 객체로 변환한 뒤 대입 연산자를 호출한다.
explicit MyClass(double data): _data(data) {};
만약 생성자에 explicit 키워드를 추가하면 다음과 같은 에러를 출력한다.

참고