Computer Science/C & C++

[C++] friend | 프렌드

lww7438 2020. 2. 27. 15:24

\(\texttt{friend}\)

프렌드

- 멤버 함수가 아닌 외부 함수를 해당 클래스의 전체 멤버에 접근 가능하게 만들어서* 해당 클래스의 멤버 함수와 동등한 접근 권한을 갖게 하는 C++의 키워드이다.
ex) 클래스 외부에 정의된 일반 함수를 \(\texttt{friend}\)를 통해 클래스 내부로 Grant하면, 클래스의 멤버 함수인 것처럼 클래스의 멤버에 접근이 가능해진다.
ex) A클래스에 B클래스를 \(\texttt{friend}\)로 Grant하면, B클래스의 멤버 함수들은 A클래스의 멤버 함수인 것 처럼 동작할 수 있다.
- \(\texttt{friend}\) 키워드로 Grant 가능한 형태로 프렌드 함수, 프렌드 클래스 ,프렌드 멤버 함수 세 가지가 있다.

※ 프렌드 함수 개념을 이용하여, 어떤 클래스의 멤버 함수를 \(\texttt{friend}\)를 통해 Grant할 수도 있다.

* 이 때, 접근 가능하게 하는 동작을 "\(\texttt{friend}\)로 Grant 하다."라고 표현한다.


\(\texttt{friend}\) 함수 정의

- Grant하고자 하는 클래스 선언부에 Grant할 \(\texttt{friend}\) 함수의 원형을 넣어야 한다.
- \(\texttt{friend}\) 키워드는 클래스 내에 들어가는 함수 원형에 붙으며, 함수 정의 부분에는 붙지 않는다.
- \(\texttt{friend}\) 함수는 기본적으로 멤버 함수가 아니기 때문에, \(\texttt{::}\)연산자를 사용할 수 없다.
- \(\texttt{friend}\) 함수는 기본적으로 멤버 함수가 아니기 때문에, 호출시에 멤버 연산자(\(\texttt{.}\) 연산자)를 사용할 수 없다.

// Ex. friend 함수의 원형, 정의, 호출 예시

class Time {
private:
...
public:
...
    friend Time operator* (double, const Time&);   // friend 키워드는 함수 원형에서만 사용한다.
// 또한, operator*() 함수가 외부 함수임을 알 수 있는 대목이기도 하다.
};

...

// 태생이 일반함수 이므로, 정의부에는 friend 키워드가 붙지 않는다.
Time operator* (double mult, const Time& t) {      // 일반 함수 이므로 Time:: 제한자 또한 붙지 않는다.
    Time result;
    long totalMinutes = t.hours * mult * 60 + t.minutes * mult;
    result.hours = totalMinutes / 60;
    result.minutes = totalMinutes % 60;
    return result;
}

...

Time A, B;

// A와 B는 적절한 값으로 초기화되었다 가정하자.

A = 2.75 *B;
// 위 구문은 컴파일러가 A = operator*(2.75, B); 로 재해석한다.

2.75.operator*(b);   // 이 구문은 당연히 안된다.


※ 교환법칙이 성립하는 연산자를 오버로딩하는 경우, 구현을 간단히 하는 방법

// Ex. 교환법칙이 성립하는 연산자를 쉽게 오버로딩 하는 예시

class Time {
...
    friend Time operator+(const Time&, double);  // 세부구현을 담당하는 함수만 Grant되면 된다.
    friend Time operator+(double, const Time&);  // 꼭 Grant되어야 하는 건 아니지만, Grant할 것을 권장한다.
};

Time operator+(const Time& t, double mult) const {...} // 세부구현을 담당하는 외부 연산자 함수

Time operator+(double mult, const Time& t) const {    // 이 버전은 굳이 friend로 Grant될 필요는 없다.
    return t * munlt;    // (객체 * double상수)는 이미 정의되어 있으므로, 간소하게 표현할 수 있다.
}

- 두 번째 버전의 연산자 함수를 굳이 Grant하는 이유는, 해당 연산자 함수도 클래스의 인터페이스임을 명시하고, 추후에 두 번째 버전의 연산자 함수를 수정할 때, \(\texttt{private}\) 멤버에 접근해야하는 경우가 생길 수도 있기 때문이다.


Friend Classes (프렌드 클래스)

- 클래스 또한, \(\texttt{friend}\)를 통해 어떤 클래스에 Grant될 수 있다.
- Grant된 클래스는 해당 클래스의 public 멤버는 물론 private, protected 멤버에도 접근할 수 있다.
- \(\texttt{friend}\) 선언은 public, protected, private 부분 어디에나 위치시킬 수 있으며, 차이는 없다.
  (즉, Grant 선언의 접근 지정자 별 차이는 없다.)

※ 프렌드 함수 개념을 이용하여, 어떤 클래스의 멤버 함수를 \(\texttt{friend}\)를 통해 Grant할 수도 있다.

Ex. Tv 클래스에 Grant된 Remote 클래스

class Tv {
public:
    friend class Remote;
    
    Tv() : volume(5) {}
    bool volup();
    bool voldown();

private:
    int volume;
};



class Remote {
public:
    Remote() {}
    bool volup(Tv& t) { return t.volup(); }
    bool voldown(Tv& t) {return t.voldown(); }
};



bool Tv::volup() {
    if (volume < 20) {
        volume++;
        return true;
    }
    else
        return false;
}

bool Tv::voldown() {
    if (volume > 0) {
        volume--;
        return true;
    }
    else
        return false;
}

- 클래스 프렌드는 위와 같이, 클래스 간의 특정한 관계를 표현하는데 유용하며,
  이 같은 수단이 없다면, Tv 클래스의 private 부분들을 public으로 구현해야 하거나,
  Tv와 Remote가 함께 들어있는 크기가 큰 클래스를 구현해야 할 것이다.
  (또한, 두 클래스를 합쳐서 구현하면, 하나의 리모콘으로 여러 텔레비전을 제어하게 할 수 없게 된다.)
- 프렌드 멤버 함수와 달리, 프렌드 클래스는 Forward Declaration을 필요로하지 않는다.
  (Tv클래스 내부의 friend 구문이 Remote라는 클래스가 존재함을 컴파일러에게 알리는 역할도 하기 때문이다.)


Friend Member Functions (프렌드 멤버 함수)

- 클래스 전체를 프렌드로 Grant하는 대신, 클래스의 몇몇 메서드만 다른 클래스에 Grant하는 것이 가능하다.

class Tv {
    friend void Remote::func();
    ...
};

- 메서드를 Grant하는 경우, 적절한 Forward Declaration이 필요하다.

class Tv;

class Remote {
    void func();   // Grant할 함수의 원형이라도 반드시 선언되어 있어야 한다.
    ...
};

class Tv {
    friend void Remote::func();
    ...
};

void Remote::func() {
    ...
}

Class friends vs Class member friends


※ private 멤버를 다루는 메서드는 해당 클래스의 멤버 함수로 구현하고,
   public 멤버를 다루는 메서드는 프렌드 클래스에서 구현하여 Grant하는 것이 범용적인 방법이다.


Mutual Friend (상호 프렌드)

- 두 클래스가 서로를 Grant한 관계를 의미한다.
- 이 경우에는 가급적 메서드 정의를 클래스 선언 뒤에 위치시키는 것이 바람직하다.

class Tv {
friend class Remote;
public:
    void func(Remote& r);   // func의 정의는 Remote 클래스 선언 뒤에 행해져야 한다.
    ...
};


class Remote {
friend class Tv;
    ...
};


void Tv::func(Remote& r) {
    ...
}

Shared Friend (공유 프렌드)

- 하나의 함수를 두 클래스에 동시에 Grant시키는 관계를 의미한다.
- 이를 통해 해당 함수는 두 클래스의 private 멤버에 모두 접근할 수 있게 된다.

class Analyzer;

class Probe {
friend void sync(Analyzer& a, const Probe& p);
friend void sync(Probe& p, const Analyzer& a);
...
};

class Analyzer {
friend void sync(Analyzer& a, const Probe& p);
friend void sync(Probe& p, const Analyzer& a);
...
};

Nested Class (내포 클래스)

- 클래스 내부에 선언된 클래스를 의미한다.
- 클래스를 Class Scope를 갖게 함으로써 이름 중복을 방지한다.
- 내포 클래스가 public 부분에 선언되고, 사용 범위 결정 연산자(::)를 이용하면 외부에서도 내포 클래스 객체를 사용할 수 있다.

class Queue {
    class Node {
    public:
        int data;
        Node* next;
        Node(const int& i) : data(i), next(0) {}
    };
    ...
};

- 외부 파일에서 내포 클래스의 메서드를 정의하기 위해선 사용 범위 결정 연산자(::)를 통해 범위를 명확히 밝혀야 한다.

Queue::Node::Node(const int& i) : data(i), next(0) {
    ...
}


내포 클래스의  Scope
private 부분에 선언된 내포 클래스: 포함한 클래스에서만 사용할 수 있다.
protected 부분에 선언된 내포 클래스: 포함한 클래스, 포함한 클래스에서 파생된 클래스에서만 사용할 수 있다.
public 부분에 선언된 내포 클래스: 클래스 외부에서도 제한자(::)를 통해 사용할 수 있다.

※ 이와 같은 Scope는 내포 구조체, 내포 열거체에도 동일하게 적용된다. 

class Team {
public:
    class Coach {...};
    ...
};

// Team 클래스 외부에서의 내포 클래스 객체 선언
Team::Coach forhire;


내포 클래스와 포함한 클래스 간의 Access Control

- 클래스를 내포시키는 것과 접근 제어는 근본적으로 관계가 없다.
- 즉, Node 클래스를 포함한 Queue 클래스라고 해서 Node 클래스에 자유롭게 접근할 수 있지 않고,
  반대의 경우도 마찬가지이다.
- 클래스 내포는 내포한 클래스의 Scope만을 통제하는 기능이다.
- 어차피, 내포된 클래스는 포함한 클래스의 Scope를 가지므로,
  내포할 클래스의 모든 멤버를 public으로 선언하는 것이 편리하다.
  (위의 Queue 클래스에 내포된 Node의 모든 멤버가 public 부분에 선언된 이유이다.)