연산자 오버로딩

Jaemyeong Lee·2024년 12월 6일

게임 서버1

목록 보기
44/220

이 Step에서 다루는 것

  • 연산자 오버로딩이 “문법 설탕”인 이유(기능 추가가 아니라 표현을 자연스럽게)
  • 멤버 함수 버전 vs 비멤버(전역) 함수 버전: 언제 무엇을 선택하는가
  • +/+=, =에서 많이 쓰는 정석 시그니처(const/참조/반환 타입)
  • “생성(초기화)” vs “대입”이 실제로 호출하는 함수가 왜 다른지
  • explicit으로 의도치 않은 암시적 변환을 막는 법

학습 목표

  • pos1 + pos2, pos += other가 어떤 함수 호출로 바뀌는지 설명할 수 있다.
  • “멤버로 만들면 10 + pos가 왜 어려운지”를 설명할 수 있다.
  • explicit이 없을 때 생길 수 있는 실수를 예시로 설명할 수 있다.

연산자 오버로딩의 목적: “의도를 보존한 채 표현만 자연스럽게”

연산자 오버로딩은 “새로운 능력”을 부여하는 기능이 아니라,
이미 가능한 일을 더 읽기 쉬운 문법으로 표현하기 위한 장치입니다.

  • 좋은 예: Position a + b (좌표 더하기), a == b (동등 비교)
  • 위험한 예: a + b가 “파괴적인 변경”을 일으키거나, 의미가 직관과 다를 때

멤버 함수 버전: pos1 + pos2의 기본형

멤버 함수로 오버로딩하면, 왼쪽 피연산자(pos1)가 곧 this가 됩니다.

class Position {
public:
    Position() = default;
    Position(int x, int y) : _x(x), _y(y) {}

    // 읽기 전용 연산은 const가 거의 정답
    Position operator+(const Position& rhs) const
    {
        return Position(_x + rhs._x, _y + rhs._y);
    }

private:
    int _x = 0;
    int _y = 0;
};

핵심 규칙:

  • 매개변수는 보통 const T&
  • 원본을 바꾸지 않는 연산은 뒤에 const
  • 결과는 보통 값으로 반환 (Position 리턴)

비멤버(전역) 함수 버전: “대칭성”과 “왼쪽 변환”을 살린다

멤버로 만들면 왼쪽이 반드시 Position이어야 합니다.

  • pos + 10은 가능(오른쪽 변환은 설계에 따라 가능)
  • 10 + pos는 왼쪽이 int라서 멤버 함수로는 처리 불가

그래서 “양쪽을 대칭으로 처리하고 싶다”면 비멤버(전역) 함수가 자연스럽습니다.

class Position {
public:
    Position() = default;
    Position(int x, int y) : _x(x), _y(y) {}

    Position& operator+=(const Position& rhs)
    {
        _x += rhs._x;
        _y += rhs._y;
        return *this;
    }

    int X() const { return _x; }
    int Y() const { return _y; }

private:
    int _x = 0;
    int _y = 0;
};

// +는 보통 +=를 활용해서 구현(중복 제거)
inline Position operator+(Position lhs, const Position& rhs)
{
    lhs += rhs;
    return lhs;
}

// 예: int + Position을 지원하고 싶다면(학습용 예시)
inline Position operator+(int a, Position b)
{
    return Position(b.X() + a, b.Y() + a);
}

정리:

  • 대칭 연산(예: +, ==) 은 비멤버가 설계가 편한 경우가 많습니다.
  • += 같은 “자기 자신을 바꾸는 연산”은 멤버 함수가 자연스럽습니다.

대입 연산자(operator=)의 핵심: “참조 반환”과 “Rule of 0”

대부분의 클래스는 직접 operator=를 구현할 필요가 없습니다. (Rule of 0)
그럼에도 규칙 자체는 알아야 합니다.

  • 반환 타입은 보통 T&
  • return *this; 덕분에 연쇄 대입이 됩니다: a = b = c;
class Position {
public:
    Position& operator=(const Position& other) = default;

private:
    int _x = 0;
    int _y = 0;
};

생성(초기화) vs 대입: 같은 =처럼 보여도 의미가 다르다

  • Position p = ...;는 “새 객체를 만드는 중”이라 생성/초기화에 가깝습니다.
  • p = ...;는 “이미 존재하는 객체를 바꿈”이라 대입입니다.

생성 vs 대입 구분 도표

┌─────────────────────────────────────────────────────────────────────────────┐
│ 같은 = 기호처럼 보여도, 호출되는 함수/의미가 다르다                            │
├─────────────────────────────────────────────────────────────────────────────┤
│  코드                               실제 의미(감각)                            │
│  ─────────────────────────────────────────────────────────────────────────   │
│  Position p = Position(1, 2);        "p를 만들면서 초기화"                      │
│                                                                              │
│  Position p;                         "p를 먼저 만든 뒤"                        │
│  p = Position(1, 2);                 "이미 있는 p에 대입"                      │
└─────────────────────────────────────────────────────────────────────────────┘

explicit: 의도치 않은 암시적 변환을 차단하는 안전장치

인자가 1개인 생성자는 “변환 생성자”가 될 수 있습니다.
explicit이 없으면, 컴파일러가 여러분 대신 조용히 변환을 시도합니다.

class Position1D {
public:
    explicit Position1D(int v) : _v(v) {}

private:
    int _v = 0;
};

void Take(Position1D p) {}

int main()
{
    // Take(10); // explicit이라면 여기서 막힘(의도치 않은 변환 방지)
}

실전 감각:

  • “그 타입으로 바꾸는 게 자연스럽고 항상 안전하다”가 아니면, explicit부터 붙인다고 생각해도 좋습니다.

체크 질문 (스스로 답해보기)

  • 10 + pos를 지원하려면 “멤버 함수”와 “비멤버 함수” 중 어느 쪽이 더 적합할까?
  • operator+const가 붙는 이유는?
  • operator+=T&를 반환하고 return *this;를 하는 이유는?
  • explicit이 없으면 어떤 형태의 버그/실수가 생길 수 있을까?

profile
李家네_공부방

0개의 댓글