z 전문가를 위한 C++ : 10 ~ 12장 :: C++, 그래픽 기술 블로그

▶ 오버라이드한 메서드의 속성 변경하기

대부분 메서드의 구현을 변경하기 위해 오버라이드 하지만, 메서드를 오버라이드하는 과정에서 메소드의 속성을 바꾸고 싶을 때도 존재합니다. 리턴 타입은 한정된 범위에 대해서 변경이 가능한데, C++ 베이스 클래스의 리턴 타입이 다른 클래스에 대한 포인터나 레퍼런스 타입이면 메서드를 오버라이드할 때 리턴 타입을 그 클래스의 파생 클래스에 대한 포인터나 레퍼런스 타입으로 바꿀 수 있으며 이러한 타입을 공변 리턴 타입(covariant return type)이라고 합니다. 베이스 클래스와 파생 클래스가 병렬 계층을 이룰 때, 기능이 유용 합니다. 다음의 예시를 봅시다

Cherry* CherryTree:pick()
{
  return new Cherry();
};

BingCherry* BingCherryTree:pick()
{
  return new BingCherry();
};

BingCherry는 Cherry의 상속을, BinCherryTree는 CherryTree의 상속을 받는다고 한다면, 리턴 타입을 변경해도 문제가 발생하지 않는다는 리스코프 치환 원칙(모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다)만 지켜진다면, 오버라이딩 가능합니다. 하지만 전혀 관련 타입으로 변경할 수는 없습니다. 즉, unique_ptr<BingCherry>형식은 감싸고 있어 unique_ptr<Cherry> 형식과 무관하다고 판단되기에 이는 사용 불가합니다.

▶ 생성자 상속

using 키워드를 사용하면 베이스 클래스에 정의된 메서드를 파생 클래스에 지정할 수 있습니다. 이는 생성자에도 적용 가능합니다.

class Base
{
  public:
    virtual void someMethod();
    virtual ~Base() = default;
    Base() = default;
    Base(std::string_view str);
};

Class Derived : public Base
{
  public:
    Derived(int i);
    virtual void someMethod(int i);
    
    using Base::someMethod; // 명시적 상속
    using Base::Base;       // 명시적 상속, string_view;
};

첫 번째로 Base::someMethod가 없다면, Derived에서 사용하는 someMethod는 상속받은 것이 아니라, 네임 스페이스에서 Base의 함수를 이름으로 가리게 되어, 인자가 없는 someMethod는 사용이 불가합니다. 따라서 Base::someMethod를 사용해주기 위해서, 명시적으로 상속받는 방법이며, 이는 생성자에도 같아,  default를 제외한 생성자들을 상속합니다. 따라서 Derived는 string_view를 받는 식으로 생성자를 호출 가능하게 됩니다. 파생 클래스에서 베이스 클래스의 생성자를 상속할 때, 파생 클래스에서 새로운 멤버 변수를 초기화해야 한다면 사용할 때, 그 멤버 변수에 대해서 멤버 초기화자를 통한 초기화가 필요시 됩니다. 또한 같은 형태의 함수나 생성자가 존재한다면using으로 가져온 생성자나 함수보다 우선시되어 작동됩니다. 또한 일부만 상속할 수도 없습니다.

▶ 베이스 클래스가 static인 경우

C++에서는 static 메서드를 오버라이드할 수 업습니다. 또한 static과 virtual을 동시에 지정할 수 없어, static 메서드를 오버라이드하면 원래 의도와 다른 효과가 발생합니다. 파생 클래스에 있는 static 메서드의 이름이 베이스 클래스의 static 메서드와 같으면 서로 다른 메서드 두 개가 생성됩니다. static 메서드는 클래스에 속하기 때문에 이름이 같더라도 각자 클래스에 있는 메서드 코드가 호출됩니다. C++에서 static 메서드를 호출할 때는 실제로 속한 객체를 찾지 않고, 컴파일 시간에 지정된 타입만 보고 호출할 메서드를 결정합니다.

▶ 베이스 클래스 메서드가 오버로드된 경우

위에서 설명한 것 중 오버로드된 메서드 중 일부만 오버라이딩 되었을 때, 이를 using을 통해 명시적으로 선언해주지 않으면, 나머지에 대해서 이름이 가려져 실행하지 못한다고 하였습니다. 하지만 이는 자식 클래스 객체를 가리킬 변수를 업캐스팅한 포인터라 레퍼런스를 활용하면 됩니다.

#include <iostream>

using namespace std;
class Base
{
	public:
		virtual ~Base() = default;
		virtual void overload() { cout << "Base's overload()" << endl; };
		virtual void overload(int i) { cout << "Base's overload(int i)" << endl; };
};

class Derived : public Base
{
	public:
		virtual void overload() override { cout << "Derived's overload()" << endl; };
};

int main() {
	Derived myDerived;

	Base& ref = myDerived;
	ref.overload(7);         // Good!
	myDerived.overload(7);   // Error!
}

이는 아마 가상 테이블 함수에서 찾아가기 때문에 다르게 작동하는 것 같습니다.

▶ private나 protected로 선언된 베이스 클래스 메서드

private이나 protected의 경우 메서드의 호출할 수 있는 대상을 제한할 뿐이지, 파생 클래스에서 부모 클래스의 private 메서드를 호출할 수 없다고 해서 오버라이드도 할 수 없는 것은 아닙니다.

class A
{
  public:
    virtual int getTotal() const { return getMoney() * getPeople(); };
    virtual int getMoney() const { return 50; };
  private:
    virtual int getPeople() const { return 4; };
};

class B : public A
{
  private:
    virtual int getPeople() const override { return 5; };
};

int main()
{
  A a;
  a.getTotal(); // 200;
  B b;
  b.getTotal(); // 250;
};

위와 같이 private의 메서드를 오버라이딩하여서 동작을 바꾸어 행동하게 만들었습니다. 

▶ 베이스 클래스 메서드와 접근 범위를 다르게 지정하는 경우

 이는 크게 범위를 좁히는 경우와 넓히는 경우로 나뉘어지는데, 좁히는 경우는 부모에서 public으로 지정하였다가 자식에서 private으로 지정하는 것인데, 이는 업캐스팅하여 해당 메서드를 실행하는 경우 인터페이스는 부모 인터페이스를 사용하지만, 함수는 가상 테이블을 거쳐 가상함수 테이블에서 자식 메서드를 사용합니다. 따라서 접근 범위 제한이 완벽하게 적용되지는 않습니다.

 범위를 넓히는 경우는, 부모에서 private으로 선언된 함수를 자식에서 public 함수에서 부르거나, 오버라이딩하는 방법이 존재합니다. 접근 범위만 바꾸고 싶다면 public에 using을 활용하여 명시적으로 상속할 수 있습니다.

▶ 파생 클래스의 복제 생성자와 대입 연산자

자식 클래스에서 명시적으로 복제 생성자를 정의하면 부모 클래스의 복제 생성자를 호출해야 하며, 사용하지 않으면 디폴트 생성자가 불립니다. 마찬가지로 파생 클래스에서 operator=을 오버라이드 하면 객체의 일부만 대입 연산을 적용하는 경우를 제외하면, 부모 버전의 대입 연산자도 호출해야 합니다.

Derived::Derived(const Derived& src) : Base(src) {};
Derived::operator=(const Derived& src)
{
  Base::operator=(src);
  return *this;
};

▶ 실행 시간 타입 정보

  • RTTI(Run Time Type Information)은 실행 시간에 객체를 들여다보는 기능을 제공합니다.
  • vtable이 없는 클래스에 대해 dynamic_cast()를 호출하면 컴파일 에러가 발생합니다.
  • typeid를 이용하여 객체의 타입을 런타임에 알아낼 수 있습니다.

▶  사용자 정의 리터럴

언더스코어 바로 다음에 나오는 첫 문자가 소문자로 나오면서 리터럴 연산자를 활용하여 가공 모드(cooked mode)로 작동됩니다.

std::complex<long double> operator"" _i(long double d)
{
  return std::complex<long double>(0, d);
}

▶  __has_include (C++17)

__has_include("파일명")을 통해 특정 헤더 파일이 포함되어 있는 지를 확인할 수 있습니다.

 

'C++공부 > 그 외의 C++' 카테고리의 다른 글

전문가를 위한 C++ : 16 ~ 18장  (0) 2022.07.14
전문가를 위한 C++ : 13 ~ 15장  (0) 2022.07.14
전문가를 위한 C++ : 4 ~ 9장  (0) 2022.07.12
전문가를 위한 C++ : 1 ~ 3장  (0) 2022.07.11
Optimized C++ 9 - 13장  (0) 2022.07.08

+ Recent posts