프로그래밍 언어/Effective C++

[Effective C++]항목 23~25 : 클래스에 대한 비멤버 함수

우향우@ 2023. 4. 10. 22:36

항목 23 : 비멤버 비프렌드로 제공가능한 함수는 그렇게 만들자 

public 함수 a,b,c를 가지고 있는 클래스 widget이 있다고 하자. 이때 클래스에 d라는 함수를 추가한다고 하자. d는 자체적인 private접근없이 a,b,c를 차례로 호출하는 기능만을 가지는 함수이다. 즉 유틸함수에 가까운 함수이다. 이때 d라는 함수를 멤버 함수로 만들 수도 있겠지만 그보단 외부에서 호출하는 비멤버 비프렌드 util함수로 선언하는 것이 좋다. d(widget& w)로 위젯을 받아 위젯의 a,b,c를 순서대로 호출하면 된다. 마치 절차지향처럼 말이다. 모순적이게도 이러한 함수 구현 방식이 클래스의 캡슐화를 향상 시킨다. 이게 무슨 소리일까?

역할군이 확실히 나누어진 객체지향 작업에서 클래스 제작자가 클래스의 내부 로직을 수정할 때 고려해야하는 것은 다음과 같다. 사용자가 public으로 주어진 인터페이스로 해당 클래스를 사용할 때 문제가 없게 만들면 된다. 이를 위해 내부 로직을 수정한다면 private, public등의 모든 멤버들의 로직을 잘 검사하고 최종적으로 public으로 인한 인터페이스 사용부분에 문제가 없는지를 검증하면 된다. 즉 클래스의 멤버들에 대한 검증을 통해 public의 출력에 대한 검증만 확실히 한다면 제작자의 역할은 끝이고 외부 사용은 이제 사용자의 몫이다. 제작자에게 수정에 의한 부담은 클래스의 멤버로 한정되며 검증해야할 멤버가 적을수록 수정 유연성, 캡슐화가 오른다는 소리이다.

여기서 d의 함수를 생각해보자. d를 만약 멤버함수로 추가한다면 이후 클래스 제작자는 클래스 로직을 수정할때마다 멤버 함수로 포함된 d도 고려해야할것이다. 비록 d가 public멤버에만 접근하여 사용자의 사용과 차이가 없다하더라도 private를 접근할 수 있는 위치에 있는 것 만으로도 고려해야하는 요소로 포함되는 것이다. 하지만 반대로 외부 util함수로써 d를 만든다면 d함수는 멤버 함수가 아닌 사용자가 된다.d가 오로지 public요소에만 접근하는 기능이기에 가능한 것이다. 이렇게 d를 외부 사용자 함수로 만든다면 클래스 제작자 입장에서는 로직 수정시 고려할 대상에서 제외되게 되고 이는 캡슐화, 수정 유연성의 향상으로 이어진다. 

정리하자면 클래스 멤버 함수 대신 외부 util 함수로 만들 수 있는 기능은 외부 util 함수로 만들어 클래스 제작자의 고려 대상을 최대한 줄이는 것이 캡슐화, 수정 유연성에 유리하다.

추가적으로 외부 함수로의 선언은 클래스 멤버 함수와 달리 여러 소스코드로 분리 해둘 수 있어 네임스페이스는 같되 소스 파일은 분리하여 체계적인 관리를 할 수 있고 더욱 편한 확장성을 가질 수 있다는 장점도 있다.

 

항목 24 : 호출자 자신도 암시적 형변환을 지원하려면 비멤버 함수로 하자.

유리수를 나타내는 클래스 Rational이 있다고하자. 이 클래스는 int하나를 받는 비explicit생성자를 가져 int로의 암시적 형변환이 가능하다. 이때 이 유리수 클래스에서 곱하기를 멤버 오퍼레이터 오버라이드 함수로 구현한다고 하자.

Rational operator*(const Rational& rhs) const;

이 멤버 함수는 Rational * int는 지원한다. int가 Rational로 형변환되기 때문이다. 하지만 int * Rational는 지원하지 않는다.

이를 위해서는 const Rational operator*(const Rational& lhs, const Rational& rhs)로 구현하면 된다.

꼭 이러한 연산자 오버라이드 함수가 아니더라도 클래스 자신과 매개변수에 대한 연산 함수에서 양쪽 모두에 대한 형변환을 지원하고 싶다면 위 방식처럼 비멤버로 선언하면 편할 것이다. 

 

항목 25 : stl:swap을 효율적인 특수화로 덮어쓰자.

stl:swap 탬플릿 함수는 타입 T 매개변수 두개를 받아 스왑하는 함수이다. 이는 tmp에 대한 복사 생성자와 두 번의 복사 대입으로 이루어진다. 만약 타입T의 복사 비용이 크다면 비용이 상당한 함수이다. 만약 클래스A가 단순히 포인터하나와 그 포인터가 가르키는 자원을 논리적으로 가진 클래스라고 하자. 클래스A의 스왑은 위의 3번의 복사가 아닌 단순히 두 객체의 포인터 멤버를 뒤바꾸는 것으로 가능하다. 이러한 경우 우리는 stl:swap을 클래스 A에 대해 특수화 처리를 해줄 수 있다.\

namespace std {

    template<>
    void swap<A>(A& a, A& b)
    {
		...//포인터 전환(멤버 함수들을 이용)
    }

}

std 네임스페이스는 특별한 네임스페이스이기에 클래스, 함수, 템플릿을 추가로 구현하는 것은 불가능하지만 기존 템플릿에 대한 특수화는 허용한다.이번엔 A가 클래스 템플릿이라고 하자. 모든 A<T>에 대해서 swap을 특수화 하려면 부분 특수화를 해야하는 함수 부분 특수화는 허용되지않는다. 그렇기에 대신 다음과 같은 방법을 사용한다. 바로 A와 같은 네임스페이스에 다음과 같이 swap을 오버로드 하는 것이다.

namespace Anamespace {
	...
    template<typename T>
    void swap(A<T>& a, A<T>& b)
    {
		...//포인터 전환(멤버 함수들을 이용)
    }

}

이렇게 만들면 swap(A<T>,A<T>)로 호출시 해당 함수가 호출된다. c++의 인자 기반 탐색 규칙에 의해 함수가 호출되면 제일 먼저 전역 함수 및 인자의 타입이 속한 네임스페이스의 함수를 먼저 찾아 실행하기 때문이다. 심지어 using std을 했더라도 이들이 우선순위를 가진다. 물론 std::swap(...)으로 호출하면 얄짤없이 std가 호출된다.

 

이제 swap 사용자 입장에서 이야기를 해보자. 어떠한 타입에 대해 swap을 호출할때 사용자는 다음과 같이 사용하면 된다.

void useSwapFuction()
{
    using std::swap;
    
    swap(obj1,obj2)

}

이렇게 하면 전역, obj1네임스페이스의 swap이 존재한다면 이들을 호출하고 이들이 없다면 std::swap의 특수화나 일반화를 호출할 것이다. 위 방식에 중 하나로 특수화가 됬다면 그것을 없다면 일반화를 호출하게 되는 것이다.

 

마지막으로 이러한 swap은 예외를 던지지 않도록 만들어야한다. 자세한 이야기는 항목29에서 이야기한다.

 

 

 

 

 

 

 

참고서적

Effective C++