본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : 연산자 오버로딩 (Operator Overloading) (98, 11, 20)

by snowoods 2025. 9. 21.

Modern C++

연산자 오버로딩 (Operator Overloading)

 

개요

연산자 오버로딩(Operator Overloading)은 C++의 강력한 기능 중 하나로, 사용자가 직접 정의한 클래스 타입에 +, -, *, ==와 같은 내장 연산자를 사용할 수 있도록 재정의하는 것을 의미합니다. 이를 통해 사용자 정의 타입도 기본 데이터 타입처럼 자연스럽고 직관적인 문법으로 다룰 수 있어 코드의 가독성과 사용 편의성이 크게 향상됩니다.

예를 들어, 두 Matrix 객체를 더할 때 m1.add(m2) 대신 m1 + m2와 같이 간결하게 표현할 수 있습니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98: 연산자 오버로딩의 기본 개념이 도입되었습니다. 대부분의 연산자를 오버로딩할 수 있는 기능이 이때부터 제공되었습니다.
  • C++11: default 키워드가 도입되어 소멸자나 기본 생성자를 컴파일러가 생성하는 버전으로 명시적으로 지정할 수 있게 되었습니다. 예제 코드의 ~Matrix() = default;가 이에 해당합니다.
  • C++20: 삼방향 비교 연산자(three-way comparison operator)인 <=>(Spaceship operator)가 도입되었습니다. 이 연산자를 오버로딩하면 모든 관계 연산자(<, >, <=, >=, ==, !=)가 자동으로 생성되어 비교 연산자 구현이 매우 간편해졌습니다.

 

내용 설명

연산자 오버로딩은 주로 클래스의 멤버 함수 또는 전역 함수(보통 friend로 선언)로 구현합니다.

  • 멤버 함수로 구현: 객체 operator 연산자 (피연산자) 형태로 호출됩니다. 이항 연산자의 경우, 왼쪽 피연산자는 this 객체가 되고 오른쪽 피연산자만 매개변수로 받습니다. (m1 + m2m1.operator+(m2)로 해석)
  • 반환 타입: operator+와 같은 연산자는 연산 결과를 담은 새로운 객체를 반환하고, operator+=와 같은 복합 대입 연산자는 자기 자신을 수정한 후 참조(*this)를 반환하여 연쇄적인 연산을 지원하는 것이 일반적입니다.
  • const 한정자: 멤버 변수를 수정하지 않는 멤버 함수(getter, print 함수 등) 뒤에 const를 붙이면, 해당 함수가 객체의 상태를 변경하지 않음을 명시할 수 있습니다. const 객체는 const 멤버 함수만 호출할 수 있습니다.

 

예제 코드

Matrix.h

2x2 행렬을 나타내는 클래스 템플릿입니다. +, +=, -, -= 연산자를 오버로딩합니다.

#pragma once

#include <iostream>

template <typename T>
class Matrix
{
public:
    Matrix();
    Matrix(const T &A, const T &B, const T &C, const T &D);
    ~Matrix() = default;

    Matrix operator+(const Matrix &rhs);
    Matrix &operator+=(const Matrix &rhs);
    Matrix operator-(const Matrix &rhs);
    Matrix &operator-=(const Matrix &rhs);

    void print_matrix() const;

    T get_A() const;
    T get_B() const;
    T get_C() const;
    T get_D() const;

    void set_A(const T &new_A);
    void set_B(const T &new_B);
    void set_C(const T &new_C);
    void set_D(const T &new_D);

private:
    T m_A;
    T m_B;
    T m_C;
    T m_D;
};

template <typename T>
Matrix<T>::Matrix() : m_A(0.0), m_B(0.0), m_C(0.0), m_D(0.0)
{
}

template <typename T>
Matrix<T>::Matrix(const T &A, const T &B, const T &C, const T &D)
    : m_A(A), m_B(B), m_C(C), m_D(D)
{
}

template <typename T>
Matrix<T> Matrix<T>::operator+(const Matrix<T> &rhs)
{
    auto result = Matrix{};

    result.set_A(this->get_A() + rhs.get_A());
    result.set_B(this->get_B() + rhs.get_B());
    result.set_C(this->get_C() + rhs.get_C());
    result.set_D(this->get_D() + rhs.get_D());

    return result;
}

template <typename T>
Matrix<T> &Matrix<T>::operator+=(const Matrix<T> &rhs)
{
    this->set_A(this->get_A() + rhs.get_A());
    this->set_B(this->get_B() + rhs.get_B());
    this->set_C(this->get_C() + rhs.get_C());
    this->set_D(this->get_D() + rhs.get_D());

    return *this;
}

template <typename T>
Matrix<T> Matrix<T>::operator-(const Matrix<T> &rhs)
{
    auto result = Matrix{};

    result.set_A(get_A() - rhs.get_A());
    result.set_B(get_B() - rhs.get_B());
    result.set_C(get_C() - rhs.get_C());
    result.set_D(get_D() - rhs.get_D());

    return result;
}

template <typename T>
Matrix<T> &Matrix<T>::operator-=(const Matrix<T> &rhs)
{
    set_A(get_A() - rhs.get_A());
    set_B(get_B() - rhs.get_B());
    set_C(get_C() - rhs.get_C());
    set_D(get_D() - rhs.get_D());

    return *this;
}

template <typename T>
void Matrix<T>::print_matrix() const
{
    std::cout << m_A << " " << m_B << '\n';
    std::cout << m_C << " " << m_D << "\n\n";
}

template <typename T>
T Matrix<T>::get_A() const
{
    return m_A;
}

template <typename T>
T Matrix<T>::get_B() const
{
    return m_B;
}

template <typename T>
T Matrix<T>::get_C() const
{
    return m_C;
}

template <typename T>
T Matrix<T>::get_D() const
{
    return m_D;
}

template <typename T>
void Matrix<T>::set_A(const T &new_A)
{
    m_A = new_A;
}

template <typename T>
void Matrix<T>::set_B(const T &new_B)
{
    m_B = new_B;
}

template <typename T>
void Matrix<T>::set_C(const T &new_C)
{
    m_C = new_C;
}

template <typename T>
void Matrix<T>::set_D(const T &new_D)
{
    m_D = new_D;
}

 

main.cpp

Matrix 객체를 생성하고 오버로딩된 연산자를 사용하는 예제입니다.

#include <iostream>

#include "Matrix.h"

int main()
{
    auto m1 = Matrix<float>(1.0, 2.0, 3.0, 4.0);
    m1.print_matrix();

    auto m2 = Matrix<float>(-1.0, -2.0, -3.0, -4.0);
    m2.print_matrix();

    auto m3 = m1 + m2;
    m3.print_matrix();
    auto m4 = m1 + m3;
    m4.print_matrix();
    m2 -= m1; // m2 = m2 - m1;
    m2.print_matrix();

    return 0;
}

 

실행 결과

1 2
3 4

-1 -2
-3 -4

0 0
0 0

1 2
3 4

-2 -4
-6 -8

 

활용팁

  • 일관성 유지: ++=처럼 쌍을 이루는 연산자는 동작에 일관성을 유지하는 것이 좋습니다. 보통 +=를 구현한 뒤, ++=를 이용해 구현하여 코드 중복을 피하고 일관성을 보장합니다. (Matrix temp(*this); temp += rhs; return temp;)
  • 전역 함수로 오버로딩: std::ostream<< 연산자를 오버로딩하여 std::cout << m1;과 같이 출력 스트림을 지원하려면 전역 함수(보통 friend로 선언)로 구현해야 합니다. 왜냐하면 연산의 왼쪽 피연산자가 std::ostream 객체이기 때문입니다.
  • 무분별한 오버로딩 지양: 연산자의 원래 의미와 전혀 다른 기능으로 오버로딩하면 코드의 혼란을 야기할 수 있습니다. 예를 들어 + 연산자로 두 객체를 삭제하는 등의 동작은 피해야 합니다.