항목 35 : 가상함수 대체제들
가상 함수는 재정의 가능한 함수를 상속하기 위해 사용한다. 이때 가상함수대신 여기에 쓸 수 있는 기법들을 살펴보자.
비가상 인터페이스
이 기법은 재정의가 필요한 부분을 private나 protected 가상 함수로 선언하고 이를 중간에 호출하여 사용하는 public 인터페이스 비가상 함수를 구현하여 제공하는 방법이다. 이렇게 하면 자식은 상속받은 private또는 protected 비가상 함수를 재정의할 것이고 사용자들은 재정의된 private함수를 사용하는 public 비가상 함수를 호출하여 사용할 것이다. 참고로 private도 자식에서 재정의는 가능하다.(호출은 불가능) 이 방식의 장점은 재정의 구현 부분 앞뒤로 기본 처리를 넣어줄 수 있다는 점이 있다.
함수 포인터
이 기법은 재정의가 필요한 함수를 가상함수가 아닌 특정 시그니처의 함수포인터 멤버 변수로 제공하는 방법이다. 그리고 생성자등을 통해 각 인스턴스별로 이 함수포인터를 채워 사용한다. 그러면 여러 인스턴스를 가져와 해당 함수 포인터 멤버를 호출하는 로직을 짠다면 다형성을 살릴 수 있다.
이 방법의 장점은 인스턴스별로 다른 구현을 줄 수 있다는 점이고 단점은 해당 함수가 외부 함수이기에 설령 매개변수로 소유자 객체 레퍼런스를 넘겨받는다 하더라도 해당 객체의 public요소밖에 접근 할 수 없다는 점이다. 이를 극복하기 위해 일부 변수를 public으로 제공할 수 있지만 부작용으로 캡슐화를 잃는다.
std::function템플릿 클래스 사용
이 기법은 위의 함수 포인터와 비슷하지만 로우 함수 포인터가 아닌 std::function을 사용한다. std::function은 템플릿 매개변수로 함수 시그니처를 받는데 함수 포인터와 달리 시그니처와 호완이 되는 함수 객체도 자동 형변환으로 받을 수 있으며 매개변수, 리턴값 타입 사이의 형변환까지 자동으로 고려하여 다른 시그니처의 함수,함수객체도 받을 수 있다.
이 방법의 장점은 함수 포인터보다 더 유연하다는 것이다. 단점은 자동 형변환이 늘 그렇듯 예상치 못한 실수가 발생할 수 있다.
고전적인 전략 패턴: 가상 함수 계층을 가진 다른 객체 소유
다른 고전적인 방법으로는 한 객체가 별개의 계층 구조의 상위 클래스 포인터 하나를 멤버로 소유하는 것이다. 소유된 클래스는 가상 함수를 하나 가지고 있으며 각종 재정의를 가진 하위 클래스들이 존재한다. 소유자 클래스는 해당 포인터의 특정 함수를 호출하여 상용한다. 여기서 생성자등을 통해 해당 멤버를 적절한 하위 클래스 인스턴스를 달아주면 해당 클래스의 정의가 사용된다.
이 기법의 장점은 함수 포인터와 같이 인스턴스별로 다른 정의를 가질 수 있게 하면서도 사용하는 정의용 클래스들이 계층 구조를 가짐으로서 좀더 체계적인 관리가 가능하다는 점이다.
항목 36 : 비가상 함수 재정의는 해선 안된다.
비가상 함수를 자식에서 재정의하면 다음과 같은 현상이 발생한다. 해당 자식 인스턴스를 부모 포인터에 담아 해당 함수를 호출하면 부모의 함수가 호출되고, 자식 포인터에 담아 호출하면 자식의 함수가 호출된다. 이 로직을 노리고 사용할 수 있다고해도 일반적으로 이런 기법은 객체지향의 정신과 위배된다. 객체 지향에서 한 인스턴스의 실제 정체가 한가지일때 어떤 포인터로 그 객체를 가르킨다고 해도 같은 함수를 호출하면 같은 처리가 일어나는 것을 기본적으로 기대하기 때문이다. 이는 객체지향을 사용하는 다른 사용자들도 똑같으므로 협업상 혼란이 발생하지 않기 위해선 이러한 부분에서 통일이 필요하다. 따라서 비가상함수는 재정의 하지말자.
항목 37 : 가상 함수에서도 상속받은 기본 매개변수 값을 다르게 해선 안된다.
위 항목에서 비 가상 함수는 재정의해서는 안된다고 했다. 그리고 가상 함수는 재정의가 가능하다. 하지만 가상 함수를 재정의할 때도 지켜야하는 것이 있다. 바로 부모에서 설정해둔 기본 매개변수 값을 다르게 설정하면 안된다는 것이다. 우선 다음과 같은 경우 어떻게 처리되는지 살펴보자.
class p
{
virtual void f(int a = 1);
}
class c: public p
{
virtual void f(int a = 2);
}
p *a = new c;
a.f();
c의 인스턴스를 p포인터에 담아 f를 매개변수 없이 호출했다. 이렇게 하면 p의 기본 매개변수 값으로 c의 f가 호출된다. 반대로 c의 포인터에 담아 f를 호출하면 c의 기본 매개변수로 c의 f가 호출된다.
즉 기본 매개변수는 현재 담긴 포인터를 기준으로 사용되는 것이다. 이는 가상함수의 호출에서 함수 자체는 동적으로 바인딩 되지만 매개변수는 정적으로 바인딩되기에 발생하는 현상이다. 이렇게 되면 같은 인스턴스더라도 어떤 포인터에 담겨있느냐에 따라 다른 처리가 발생할 수 있다. 따라서 기본 매개변수는 통일할 필요가있다.
하지만 여기서 모든 기본 매개변수를 하드 코딩으로 통일시키는 건 좋지않다. 변경이 필요할 때 많은 부분을 일일히 수정해야하기 때문이다. 이를 방지하기 위해선 위에서 배운 비가상 인터페이스를 사용하면 된다. virtual private로 함수를 기본 매개변수 없이 그대로 구현하고 이를 호출하는 비가상 인터페이스 함수를 만든다. 해당 함수는 virtual함수의 매개변수를 그대로 가지되 기본 매개변수를 가진다. 그리고 그 매개변수를 그대로 넘겨 가상 함수를 호출하는 것이다. 이렇게 하면 해당 인터페이스로 매개변수없이 호출시 설정해둔 기본 매개변수가 넘어가며 해당 가상 함수가 호출된다.
참고서적
Effective C++
'프로그래밍 언어 > Effective C++' 카테고리의 다른 글
[Effective C++]항목 41~44: 템플릿 1 (0) | 2023.06.10 |
---|---|
[Effective C++]항목 38~40: 객체 지향 설계 3 (0) | 2023.06.01 |
[Effective C++]항목 32~34: 객체 지향 설계 1 (0) | 2023.05.15 |
[Effective C++]항목 29~31: 구현 2 (0) | 2023.04.27 |
[Effective C++]항목 26~28: 구현 1 (0) | 2023.04.27 |