C++/C++ : Study

8. 연산자 오버로딩 (4) - 전역 함수로의 구현

더블유제이플로어 2025. 6. 15. 00:41

Binary operator, as Global Function

◆  이항(binary) 연산자의 전역 함수로의 선언 (+, -, ==,!=, > , < , etc)

   ●  Operator 오버로딩을 전역 함수로 선언(Point::operator 가 아님!)
   ●  Lhs 도 매개변수로써 전달
        ☞    이러한 구현을 위해서는 함수를 friend로 선언하는 것이 일반적

    Point operator+(const Point& lhs, const Point& rhs);
    Point operator-(const Point& lhs, const Point& rhs);
    Point operator==(const Point& lhs, const Point& rhs);
    Point operator<(const Point & lhs, const Point & rhs);
    
    Point p1{ 10,20 };
    Point p2{ 30,40 };
    Point p3 = p1 + p2; // operator+(p1,p2) or p1.operator+(p2)
    p3 = p1 - p2; // operator-(p1,p2) or p1.operator-(p2)

    if(p1==p2) // operator==(p1,p2) or p1.operator==(p2)
        ...
이러한 명령문을 작성하면, 컴파일러는 사실 오른쪽 주석 두 개 후보 중 호출 가능한 것을 찾습니다.

✅ 이항 연산자의 전역 함수 오버로딩 정리 (+ 강의자료 통합)

🔷 전역 함수 방식이란?

이항 연산자 오버로딩을 멤버 함수가 아닌, 전역 함수(비멤버) 로 정의하는 방법.
Point operator+(const Point& lhs, const Point& rhs);

이 방식에서는 lhs, rhs 모두 함수의 인자로 들어오며,
클래스 외부 함수에서 구현된다

🧠 왜 전역 함수로 오버로딩할까?

✅ 교환 법칙을 구현할 수 있기 때문!

Point p1{10, 20};
Point p2{30, 40};

Point p3 = p1 + p2;  // OK: 멤버 함수 or 전역 함수
Point p4 = p2 + p1;  // OK: 전역 함수이면 좌우 위치 무관

멤버 함수는 좌측 피연산자만 해당 클래스 객체여야 작동
하지만 전역 함수는 lhs, rhs 모두 자유롭게 설계 가능

📌 전역 함수의 일반적 형식

Point operator+(const Point& lhs, const Point& rhs);
Point operator-(const Point& lhs, const Point& rhs);
bool operator==(const Point& lhs, const Point& rhs);
bool operator!=(const Point& lhs, const Point& rhs);
bool operator<(const Point& lhs, const Point& rhs);

모두 인자를 2개 받으며, 멤버 함수가 아니라는 점이 중요!

❗ private 멤버 접근 문제

전역 함수에서는 클래스의 private 멤버(xPos, yPos)에 직접 접근할 수 없다.

Point operator+(const Point& lhs, const Point& rhs) {
    return Point{lhs.xPos + rhs.xPos, lhs.yPos + rhs.yPos};  // ❌ 에러
}

🔐 해결법: friend 선언

class Point {
private:
    int xPos;
    int yPos;

public:
    friend Point operator+(const Point& lhs, const Point& rhs);
};

friend로 선언하면 전역 함수가 클래스의 private 멤버에도 접근 가능해진다.

⚙️ 컴파일러가 어떤 함수를 선택하나?

Point p3 = p1 + p2;

이 문장이 있으면 컴파일러는 다음 2가지 후보를 모두 찾아본다:

  1. p1.operator+(p2) (멤버 함수)
  2. operator+(p1, p2) (전역 함수)
💡 둘 중 하나라도 있으면 호출 가능
둘 다 있으면 →오버로드 우선순위에 따라 선택 (보통 멤버 함수 우선)

🧩 namespace 관련 질문 정리

“전역 함수인데 namespace가 들어가면 애매하다”는 말은?
  • 전역 함수라 해도 namespace 안에 있으면 진짜 ‘전역(global)’이 아님
  • 예: my::operator+는 using namespace my; 없이 p1 + p2에 바로 적용되지 않음
  • 그래서 “전역 함수”라고 할 때는 사용 범위가 열려 있다는 느낌으로 이해하면 된다.

⚠️ 잘못된 해결법: public으로 풀기?

class Point {
public:
    int xPos; // ❌ 은닉 위반
};
  • 이렇게 하면 전역 함수에서 접근은 가능하지만
  • 객체지향의 캡슐화, 정보은닉 원칙 위배
  • ⇒ 이러면 “그냥 구조체 쓰지 왜 클래스를 쓰냐?”는 소리 나옴

✅ 핵심 요약

구분 멤버 함수 오버로딩  전역 함수 오버로딩 
피연산자 수 1개 (this + 1 인자) 2개 모두 인자로 받음
위치 제약 좌측 피연산자가 클래스 객체여야 함 없음 (교환 가능)
private 접근 직접 가능 friend 선언 필요
사용 예 p1 + p2 (p1이 클래스) 3 + p1, p1 + 3 등 자유롭게

✅ 결론

전역 함수 방식의 연산자 오버로딩은 멤버 함수 방식보다 피연산자의 자유도가 높으며,
특히 3 * p1 같은 표현이나 교환 법칙 구현이 필요한 상황에서 필수적이다.
단, private 멤버를 사용하는 경우에는 friend 선언을 통해 클래스 내부 접근 권한을 부여해야 한다.

중요한 Part이다.

   ●  +연산자의 오버로딩

    friend Point operator+(const Point& lhs, const Point& rhs)
    {
        return Point{ lhs.xPos + rhs.xPos, lhs.yPos + rhs.yPos };
    }
    
    Point p3 = p1 + p2 ; // operator+(p1,p2);
friend : operator+ 함수는 Point 클래스의 멤버 함수가 아니기 때문에 xPos와 yPos에 쉽게 접근하기 위해 friend 선언합니다.
전역함수가 호출되었으므로, p1과 p2의 xPos , yPos를 더한 값을 가지고 있는 객체가 반환되고, 그 객체는 p3에 저장됩니다.
operator+ 함수는 Point 클래스의 멤버 함수가 아님을 다시 한번 말합니다.

✅ 강의 내용

operator+ 함수는 Point 클래스의 멤버 함수가 아니기 때문에
xPos, yPos에 쉽게 접근하려면 friend 선언이 필요하다.

🤔 궁금증

전역 함수인데 왜 클래스 안에 있을까?
클래스 안에 선언하면 멤버 함수 아닐까?
friend Point operator*() 이런 식으로 클래스 안에 정의된 함수는 멤버 함수 아닌가?
const를 뒤에 못 붙이는 것도 헷갈린다.

🧠 해설

📌 Q1. 클래스 안에 있는 friend 함수는 멤버 함수인가?

❌ 아니다. 전역 함수다.
class Point {
    friend Point operator+(const Point& lhs, const Point& rhs); // 전역 함수임
};
  • 클래스 내부에 정의돼 있어도, this가 없고 인자 2개를 직접 받는다.
  • 단지 Point 클래스에 접근 권한을 가진 전역 함수일 뿐이다.
  • "나 멤버 함 아님!"이라고 friend가 직접 말해주는 구조

📌 Q2. const를 뒤에 못 붙이는 이유는?

Point operator*(int scale, const Point& rhs) const; // ❌ 전역 함수에서는 컴파일 에러
  • 뒤에 붙는 const는 멤버 함수 전용 문법
  • 의미: "내부 멤버 안 바꿀게요!"
  • 전역 함수에는 this가 없기 때문에 붙일 의미 자체가 없어
  • 그래서 전역 함수는 const를 인자에만 쓸 수 있고, 함수 자체엔 못 붙임

✅ 강의 내용

Point p3 = p1 + p2; 처럼 썼을 때, 컴파일러는

1. p1.operator+(p2)
2. operator+(p1, p2)
둘 중 호출 가능한 걸 찾는다.

🤔 궁금증

둘 다 있으면 어떤 게 우선으로 호출될까?
전역 함수 먼저 쓰면 멤버 함수는 무시될까?

🧠 해설

📌 Q3. 멤버 vs 전역 함수 중 누가 먼저?

  • 멤버 함수가 있으면 우선적으로 사용된다
  • 없으면 전역 함수로 넘어감
// 1순위
Point Point::operator+(const Point& rhs) const;

// 2순위
friend Point operator+(const Point& lhs, const Point& rhs);

📌 그래서 실무에서는 멤버 함수로 충분하면 그것만 쓰고,
교환법칙이 필요한 경우 (int + Point)는 전역 함수로 보완하는 방식이 일반적이다.

✅ 설명

friend를 쓰지 않으면 xPos, yPos에 접근할 수 없다.
public으로 만들면 정보 은닉이 깨진다.

🤔 궁금증

private이라 접근 안 된다는 게 무슨 말일까?
객체로는 접근이 불가능하다는 표현이 헷갈린다.

🧠 해설

📌 Q4. 전역 함수에서 private 멤버 접근 못 하는 이유?

Point operator+(const Point& lhs, const Point& rhs) {
    return Point{ lhs.xPos + rhs.xPos, lhs.yPos + rhs.yPos }; // ❌ 오류
}
  • 클래스 바깥에서는 private 멤버를 직접 접근할 수 없음
  • 객체로는 접근 불가능하다라는 표현보단
    👉 "클래스 외부에서는 private 멤버에 접근할 수 없다"라고 이해해야 한다.

📚 정리 요약 (학습노트용)

질문 답변
friend 함수가 멤버 함수인가요? ❌ 전역 함수지만 private 접근 권한을 부여받음
클래스 안에 있어도 멤버 함수인가요? ❌ 인자를 다 받고 this가 없으면 전역 함수
const Point& 인자는 왜 쓰나요? 값 보호 + 복사 비용 줄이기
전역 함수 뒤에 const 못 붙이나요? ✅ 멤버 함수만 가능, 전역 함수는 의미 없음
p1 + p2 쓸 때 컴파일러는 뭘 먼저 찾나요? 멤버 함수 → 전역 함수 순서
전역 함수에서 private 멤버 접근하려면? friend 선언 필요
public으로 바꾸면 안 되나요? ❌ 정보 은닉 위배, 객체지향 원칙 깨짐

   ●  Friend 유의사항

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

    friend Point operator+(const Point& lhs, const Point& rhs)
    {
        return Point{ lhs.xPos + rhs.xPos, lhs.yPos + rhs.yPos };
    }
};
operator+ 함수 코드가 class Point {...}; 안에 있다고 해서 무조건 멤버 함수가 아니다.
class Point {
private:
    int xPos;
    int yPos;
public:
    Point(int x, int y)
        :xPos{ x },yPos{y}
    { }

    friend Point operator+(const Point& lhs, const Point& rhs)
    { }
};

Point operator+(const Point& lhs, const Point& rhs)
{
    return Point{ lhs.xPos + rhs.xPos, lhs.yPos + rhs.yPos };
}
위에 코드는 단지 왼쪽의 코드를 줄여 쓴 것뿐이다. operator+는 전역 함수이고, class는 이 함수를 friend 선언한 것을 축약해서 쓴 것이다.

📌 friend 전역 함수는 멤버 함수가 아니다

Point 클래스 내부에 다음과 같이 작성된 friend 함수가 있다고 하더라도, 이는 멤버 함수가 아니다.

이 함수는 Point 클래스 외부에 정의되는 전역 함수이며,
단지 friend 키워드를 통해 Point 클래스의 private 멤버(xPos, yPos)에 접근할 수 있는 접근 권한만 부여받은 것이다.

클래스 내부에 선언되어 있더라도 this 포인터를 가지지 않기 때문에 멤버 함수로 간주되지 않는다.
즉, 이 함수는 클래스의 멤버 함수가 아닌, 전역 함수(free function)이다.


   ●  +연산자의 오버로딩 동작 방식의 이해

 friend Point operator+(const Point& lhs, const Point& rhs)
 
 Point p3 = p1 + p2; // operator+(p1,p2)
 ...

ChatGPT 가 만든 Point_asGlobal_Binary_Plus.cpp 예제 코드

<< 연산자도 추가하였다.

#include <iostream>

// Point 클래스 정의
class Point {
private:
    int xPos;
    int yPos;

public:
    // 생성자
    Point(int x = 0, int y = 0)
        : xPos{x}, yPos{y} {}

    // operator+ 연산자 friend 선언
    friend Point operator+(const Point& lhs, const Point& rhs);

    // operator<< 출력 연산자 friend 선언
    friend std::ostream& operator<<(std::ostream& os, const Point& pt);
};

// 전역 함수로 + 연산자 오버로딩
Point operator+(const Point& lhs, const Point& rhs) {
    return Point{ lhs.xPos + rhs.xPos, lhs.yPos + rhs.yPos };
}

// 전역 함수로 << 출력 연산자 오버로딩
std::ostream& operator<<(std::ostream& os, const Point& pt) {
    os << "(" << pt.xPos << ", " << pt.yPos << ")";
    return os;
}

int main() {
    Point p1{10, 20};
    Point p2{30, 40};

    // + 연산자: 전역 함수
    Point p3 = p1 + p2;

    // << 연산자: 전역 함수
    std::cout << "p1 = " << p1 << "\n";
    std::cout << "p2 = " << p2 << "\n";
    std::cout << "p1 + p2 = " << p3 << "\n";

    return 0;
}

📌 설명 요약

연산자 구현 방식 반환형 설명
+ 전역 함수 Point 두 점 좌표 덧셈
<< 전역 함수 std::ostream& 객체 상태를 출력 스트림에 전달

✅ std::ostream로 선언해야 하는 이유

🔹 1. << 연산자는 ostream 객체의 연산자 오버로딩 함수이다

C++에서 cout << obj;와 같이 사용할 때,
실제로는 다음과 같은 함수 호출로 해석된다:

operator<<(std::ostream& os, const YourType& obj)

즉, << 연산자는 std::ostream 타입의 첫 번째 인자를 받는 전역 함수 형태로 오버로딩되어 있어야 한다.
왜냐하면 cout은 std::ostream 클래스의 객체이고,
사용자 정의 타입(Point)을 출력하기 위해서는 ostream에 맞는 함수 시그니처를 가져야 하기 때문이다.

🔹 2. std::ostream은 C++ 표준 라이브러리의 클래스이다

  • ostream은 std 네임스페이스에 정의되어 있음
  • 따라서 정확한 타입을 명시하려면 std::ostream을 써야 한다
namespace std {
    class ostream {
        ...
    };
}

🔹 3. using namespace std;를 쓰면 어떻게 되나?

using namespace std;
  • 이 선언을 하면 코드 전체에서 std::를 생략해도 됨
  • 따라서 std::ostream 대신 단순히 ostream이라고 써도 문법적으로는 문제없다
ostream& operator<<(ostream& os, const Point& pt); // 가능

🔸 그럼에도 불구하고 std::ostream을 명시하는 이유

  1. 명시적인 코드가 가독성이 더 좋다
    → 이 함수가 명확히 C++ 표준 라이브러리 타입을 다룬다는 걸 보여줌
  2. 헤더 파일에서 using namespace std;는 지양됨
    → 특히 라이브러리/공용 코드에선 네임스페이스 충돌 위험이 있으므로
    std::를 명시하는 것이 더 안전하고 명확한 코드 작성법이다
  3. 협업 코드/오픈소스에선 std 명시가 관례적
💡 따라서, 항상 std::ostream을 명시하는 형태가 더 좋다.

📌 결론 요약

질문 답변
왜 std::ostream을 써야 하나? ostream은 std 네임스페이스 안에 있는 타입이기 때문에 명시해야 한다
using namespace std; 쓰면 ostream만 써도 되나? 문법적으로는 가능하지만, 명확성과 충돌 방지를 위해 std::ostream을 사용하는 것이 바람직하다
출력 연산자 오버로딩 시 함수 시그니처는? std::ostream& operator<<(std::ostream& os, const Point& pt);
std::ostream은 C++ 표준 라이브러리에 정의된 출력 스트림 타입이다. 출력 연산자 <<를 오버로딩할 때는, 첫 번째 인자로 반드시 std::ostream&을 받아야 cout << obj;와 같은 구문이 동작할 수 있다. using namespace std;를 선언하면 ostream으로 줄여 쓸 수는 있지만, 코드의 명확성과 충돌 방지를 위해 std::ostream을 직접 명시하는 것이 바람직하다.

   ●  * 연산자의 구현
        ☞    3*p1도 가능하도록 하기 위해 전역 함수를 추가 구현   
   ●  마찬가지로, 함수의 friend를 가정함
        ☞    friend 가 아닌 경우 lhs.xPos , rhs.xPos 접근 불가

    Point Point::operator*(int scale, const Point& rhs)
    {
        return Point{ xPos * scale, yPos * scale };
    } // p1*3 이 진입하는 멤버 함수

friend Point operator*(int scale, const Point& rhs)
    {
        return rhs * scale;
    } // 3*p1 이 진입하는 전역 함수

✅ 연산자 오버로딩의 교환법칙 구현

Point p3 = 3 * p1;와 같이 왼쪽 피연산자가 int 타입인 경우,
이는 3.operator*(p1) 형태로는 해석할 수 없기 때문에 문법적으로 성립하지 않는다.

이를 해결하려면 다음과 같은 전역 함수를 만들어야 한다:

Point operator*(int scale, const Point& rhs);

이 전역 함수는 3 * p1에서 scale = 3, rhs = p1로 해석되어 호출이 가능하다.

✅ 클래스 코드 예시

class Point {
private:
    int xPos;
    int yPos;

public:
    Point(int x=0, int y=0)
        : xPos{x}, yPos{y} {}

    // 멤버 함수
    Point operator*(int scale) const {
        return Point{ xPos * scale, yPos * scale };
    }

    // friend 전역 함수 (좌우 순서 자유롭게)
    friend Point operator*(const Point& rhs, int scale);
    friend Point operator*(int scale, const Point& rhs);
};

// 전역 함수 정의
Point operator*(const Point& rhs, int scale) {
    return Point{ rhs.xPos * scale, rhs.yPos * scale };
}

Point operator*(int scale, const Point& rhs) {
    return Point{ rhs.xPos * scale, rhs.yPos * scale };
}

이와 같이 구현하면 p1 * 3, 3 * p1 모두 정상적으로 작동하며,
사용자는 연산자 위치를 기억할 필요 없이 교환법칙처럼 자연스럽게 사용할 수 있다.

✅ 컴파일러가 모호성을 판단하는 예시

Point p3 = p1 * 3;

이 표현은 내부적으로 두 가지 해석이 동시에 가능하다:

  1. p1.operator*(3) (멤버 함수)
  2. operator*(p1, 3) (전역 함수)

이 경우 컴파일러는 두 함수 모두 호출 조건을 만족하므로
"‘operator *’ is ambiguous" (모호하다)**는 컴파일 에러를 발생시킨다.

❓friend 함수에 바디를 클래스 안에 써도 돼? 현업에서도 그렇게 쓰는지?

답변:
C++ 문법상 friend 전역 함수는 클래스 내부에 바디까지 작성해도 문제없다.
하지만 현업이나 팀 프로젝트에서는 클래스 정의와 구현을 분리하는 것이 일반적이다.

보통은 클래스 안에는 다음처럼 선언만 넣고,

class Point {
    friend Point operator*(int scale, const Point& rhs);
};

바디는 .cpp 파일이나 클래스 외부에 따로 정의하는 것이 일반적이다.
즉, 클래스 안에 정의까지 쓰는 건 예제나 교육 자료에서 지면을 아끼기 위한 경우가 많다.

❓ 멤버 함수가 먼저 호출된다고 했는데, 지금 에러 보면 아닌 것 같아. 틀린 거 아닌가?

답변:
컴파일러는 멤버 함수 우선으로 탐색하지만,
전역 함수가 동시에 정확히 같은 시그니처로 호출 가능할 경우,
오버로딩 해석에서 모호성(ambiguity) 에러를 발생시킨다.

즉, “멤버 함수 먼저 본다 → 둘 다 가능하면 멈추지 않고 둘 다 비교 → 충돌 시 에러”가 맞는 흐름이다.
둘 중 하나는 반드시 제거해야 한다.

🧷 요약 문장

  • 3 * p1 형태는 int 타입에는 멤버 함수가 없기 때문에 operator*(3, p1) 형태의 전역 함수로만 처리할 수 있다.
  • 전역 함수와 멤버 함수가 동시에 존재하고 호출 조건이 중복되면, 컴파일러는 모호성 에러를 발생시킨다.
  • 이를 방지하려면 같은 의미의 연산자 오버로딩은 하나의 방식으로만 구현하는 것이 안전하다.
  • friend 전역 함수는 클래스 내부에 정의해도 되지만, 실무에서는 보통 클래스 외부에 정의한다.

ChatGPT 가 만든 Point_asGlobal_Binary_mul_And_Unary_Increment.cpp

#include <iostream>

class Point {
private:
    int xPos;
    int yPos;

public:
    // 생성자
    Point(int x = 0, int y = 0)
        : xPos{x}, yPos{y} {}

    // 전위 ++ 연산자 (단항, 멤버 함수로 구현)
    Point& operator++() {
        ++xPos;
        ++yPos;
        return *this;
    }

    // 출력 연산자 friend 선언
    friend std::ostream& operator<<(std::ostream& os, const Point& pt);

    // 전역 operator* 사용을 위한 friend 선언
    friend Point operator*(const Point& lhs, int scale);
    friend Point operator*(int scale, const Point& rhs);
};

// 전역 함수: 출력 연산자 <<
std::ostream& operator<<(std::ostream& os, const Point& pt) {
    os << "(" << pt.xPos << ", " << pt.yPos << ")";
    return os;
}

// 전역 함수: Point * int
Point operator*(const Point& lhs, int scale) {
    return Point{ lhs.xPos * scale, lhs.yPos * scale };
}

// 전역 함수: int * Point
Point operator*(int scale, const Point& rhs) {
    return Point{ rhs.xPos * scale, rhs.yPos * scale };
}

int main() {
    Point p1{3, 4};
    Point p2 = p1 * 2;   // 전역 operator*(Point, int)
    Point p3 = 2 * p1;   // 전역 operator*(int, Point)

    std::cout << "p1 = " << p1 << "\n";
    std::cout << "p2 = " << p2 << "\n";
    std::cout << "p3 = " << p3 << "\n";

    ++p1;  // 전위 ++ 연산자
    std::cout << "After ++p1: " << p1 << "\n";

    return 0;
}

📌 설명 요약

항목 구현 방식 설명
Point * int 전역 함수 좌측 피연산자가 Point
int * Point 전역 함수 좌측 피연산자가 int
++p1 멤버 함수 단항 전위 연산자 오버로딩
<< 전역 함수 출력 스트림 오버로딩

🧠 해설 문장

  • 이항 곱셈 연산자 *를 전역 함수로 오버로딩하면 p * 3뿐 아니라 3 * p와 같이 좌우 피연산자 위치가 달라도 연산이 가능하다.
  • 출력 연산자 <<는 std::ostream 타입의 첫 번째 인자를 받는 전역 함수로 정의해야 cout << obj 형태로 출력이 가능하다.
  • 전위 증감 연산자 ++p1는 멤버 함수로 정의하며, 객체 자체를 변경하고 참조를 반환하는 방식으로 구현한다.

Operator Overloading

◆  연산자 오버로딩 : 클래스에 대한 연산자의 적용 방식을 사용자가 직접 오버로딩하여 구현할 수 있다.

◆  멤버 한수인 연산자 오버로딩 : 클래스의 멤버함수로 operatorX()라는 이름을 갖는 함수를 구현하여 연산자를 오버로딩 할 수 있다. 이때 이항 연산자의 경우 우측 피연산자는 인자로 넘어온다.
◆  전역 함수인 연산자 오버로딩 : 멤버 함수로 구현 시 교환 법칙 문제가 발생할 수 있고, 이러한 경우 전역 함수로 오버로딩하며, 이때 friend 키워드를 사용하면 편리함
◆  스트림 삽입 및 추출 연산자 오버로딩
◆  대입 연산자 오버로딩
◆  첨자 연상자 오버로딩

🔹 멤버 함수로만 구현할 때의 한계

  • 연산자 오버로딩을 멤버 함수로만 구현하면 다음과 같은 문제가 발생할 수 있다.
Point p1{10, 20};
Point p2 = p1 * 3;  // 가능 (p1.operator*(3))
Point p3 = 3 * p1;  // 오류 (3.operator*(p1)은 불가능)
  • 3은 리터럴이자 기본형(int) 타입이므로, 멤버 함수가 존재하지 않는다.
  • 따라서 3 * p1과 같이 사용하면 컴파일러는 멤버 함수를 찾지 못하고 에러를 발생시킨다.

🔹 이 문제를 "교환 법칙 문제"라고 부른다

  • 수학적으로 p1 * 3과 3 * p1은 같은 결과를 기대하지만,
  • C++에서는 양쪽 피연산자의 타입에 따라 다르게 동작하므로,
  • 실제 코드에서 두 표현이 모두 성립하지 않는 경우를 교환 법칙 문제가 있다고 말한다.

🔹 해결 방법: 전역 함수 + friend 키워드

  • 전역 함수로 operator*를 오버로딩하면 3 * p1, p1 * 3 둘 다 구현 가능하다.
  • 그러나 operator*는 클래스 외부에서 정의되므로 private 멤버 변수에 접근할 수 없다.
  • 이때 가장 흔하게 쓰이는 방법이 friend 함수로 선언하여 접근을 허용하는 방식이다.

❓friend 를 쓰지 않고 private 멤버에 접근하지 않고 구현하는 방법은 없을까?

✔ 답변:

1. public 접근자(getter)를 제공하는 방법

 

class Point {
private:
    int xPos;
    int yPos;

public:
    Point(int x = 0, int y = 0) : xPos{x}, yPos{y} {}

    int getX() const { return xPos; }
    int getY() const { return yPos; }
};

Point operator*(int scale, const Point& rhs) {
    return Point{ rhs.getX() * scale, rhs.getY() * scale };
}

 

  • 이 방식은 friend를 사용하지 않고도 외부에서 멤버 변수 값을 안전하게 접근할 수 있게 해준다.
  • getX(), getY()를 통해 간접 접근이 가능하다.
  • 하지만 단점은 코드가 다소 길어지고, 호출 비용이 추가된다는 점이다.

2. public 으로 멤버 변수를 노출하는 방법 → ❌ 비권장

// 나쁜 예
class Point {
public:
    int xPos;
    int yPos;
};

 

 

  • 이 방법은 간단하지만, 정보 은닉 원칙에 위배된다.
  • 외부에서 무분별하게 멤버 값을 수정할 수 있으므로 객체지향 설계 철학에 맞지 않는다.

✅ 결론

  • friend를 남용하면 캡슐화가 깨질 수 있지만,
  • 출력 연산자나 교환 법칙 문제 해결 같은 정당한 상황에서는 사용하는 것이 일반적이다.
  • 객체지향 원칙을 지키면서 구현하려면 getter를 사용하는 방식이 가장 타협적인 방법이다.
전역 함수로 연산자 오버로딩을 구현하는 경우 private 멤버에 접근할 수 없다. 이때 friend 키워드를 사용하면 전역 함수에서도 클래스 내부 멤버에 접근이 가능하다. 하지만 friend 사용을 지양하고 싶다면, getter를 만들어서 안전하게 값을 접근할 수 있도록 구성할 수도 있다. 이 방식은 객체지향 원칙을 지키는 대안이 될 수 있다.

 

https://youtu.be/s_UdO2fofSw?si=me-AdLtxgGHDC1qo