
언리얼 엔진을 주로 활용하다보면 다음과 같은 메소드들이 주로 발견된다.
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
기본적인 return type으로 void가 적히는 것(아무것도 return하지 않음)을 표현하는 것은 이해하나 virtual(가상의)을 추가하는 것에 대해 잘 이해하지 못해 더 알아보고 기록한 내용이다.
virtual에 대해 더 알아보기 전에 우선 C++도 객체 지향 언어임을 인지해야 한다. C++은 객체 지향 언어이기 때문에 다른 객체 지향 언어(Java, C#...)과 동일하게 객체 지향 언어의 주요한 특징인 캡슐화, 다형성, 상속의 특성을 띄고 있다.
virtual function은 그 중 상속과 관련되어 있다.
흔히 상속이라는 개념은 드라마 같은 곳에서 재벌이 자식들에게 재산을 상속한다라는 말로 많이 접해온 단어이다. 프로그래밍에서도 이와 굉장히 유사한 특징을 가지고 있다.
// Parent.java
public class Parent {
public int Money;
public void PayMoney() {
System.out.println("ㅇㅇ");
}
}
// Child.java
public class Child extends Parent {
@Override
public void PayMoney() {
super.PayMoney();
System.out.println("ㅎㅎ");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.PayMoney();
// ㅇㅇ
// ㅎㅎ
}
}
위의 코드는 자바다. Parent에서 필요한 변수나 함수(메소드)를 선언하고 그 것을 그대로 자식한테 물려주는 것이 가능하고 Java의 경우 부모의 함수를 super.PayMoney로 호출해 부모의 함수와 자식의 함수 둘 다 호출하게 할 수 있다.
객체지향 언어에서는 이 상속을 이용해 동일한 코드를 상위 객체 즉 부모 객체로 만들어 필요에 따라 자식 객체들을 만들어 상속시킴으로써 재사용성을 늘리는 방식을 택한다.
C++에서도 Java처럼 객체 지향을 추구하기에 상속 개념, override개념이 있으나 virtual 을 사용하는 이유 자체는 조금 더 복잡한 사연이 존재한다.
// Parent.h
class Parent {
public:
// C++에서는 header 파일에서도 직접 함수 초기화 선언이 가능하다.
void PayMoney() {
std::cout << "HELLO" << std::endl;
}
};
// Child.h
class Child : public Parent {
public:
void PayMoney() {
std::cout << "BYE" << std::endl;
}
};
// Main.cpp
int main() {
Parent NewParent;
Child NewChild;
Parent* NewParentPtr = &NewParent;
Child* NewChildPtr = &NewChild;
NewParentPtr->PayMoney(); // HELLO
NewChildPtr->PayMoney(); // BYE
}
위의 코드는 C++에서 사용하는 상속의 방식이다. 상속을 할 때 굳이 포인터를 사용해서 상속을 할 필요는 없으나 예시를 더 자세히 보기 위해서는 포인터를 사용하는 것이 좋아 사용하였다.
자바랑 동일하게 부모 함수를 만들고 자식 함수도 동일한 이름을 줌으로써 기존의 부모 함수를 물려받아 그 함수 기반으로 새로운 동작을 정의하는 것이 가능하다.
다만 위의 자바와 2개가 차이가 나는데 super와 override가 존재하지 않는다. C++에서 virtual 을 사용해야하는 이유가 여기서 드러나게 된다.
이전의 C++코드에서 다음과 같이 변경해보고 PayMoney 메소드를 출력해본다.
// AS-IS
Child* NewChildPtr = &NewChild;
// TO-BE
Parent* NewChildPtr = &NewChild;
.
.
.
결과는 아마 우리 예상과는 다르게 Hello\n Hello가 나올 것이다. C++은 자바와 다르게 Value가 아닌 변수 타입에 따라 함수가 호출되게 된다. 우리가 보기에는 실제 사용하는 값이 Child Type이기에 BYE라는 결과가 나오기를 바랄 수 있다(아마?). 하지만 C++은 변수 타입을 따라가기 때문에 실제 값이 상속받은 자식 타입이더라도 부모의 함수가 호출되는 결과가 나오게 된다.
그렇다면 타입이 다르더라고 자식 함수의 결과를 나오게 하려면 어떻게 해야하는가를 virtual 선언을 통해 할 수 있다.
class Parent {
public:
virtual void PayMoney() {
std::cout << "HELLO" << std::endl;
}
};
부모 클래스의 메소드에 virtual 을 선언하고 기존의 PayMoney를 돌려보게 된다면 우리가 원하는 결과인 HELLO\nBYE가 노출되는 것을 확인할 수 있다.
virtual은 부모 객체에서 선언을 통해 자식 함수에 영향을 끼치지 않겠다라는 명시적인 표현이 되기에 자식 함수의 결과만을 출력시키는 것이 가능하다.
- C++ 에서 자식 함수에서 기존 부모 함수를 호출하려면 어떻게 하는가?
일반 C++에서는 Super 키워드는 존재하지 않는다. 아마 새로 코드를 짜야하는 것 처럼 보이는데, Unreal Engine에서는 Super 키워드를 따로 만들어서 Super::(메소드) 형태로 사용은 가능하다.
그리고 override 또한 사용이 가능하다.
class Child : public Parent {
public:
void PayMoney() override {
std::cout << "BYE" << std::endl;
}
};
override는 자식 클래스에 붙이는 것으로 실제로 무슨 역할을 하지는 않으나, 오타를 방지하는 것에 도움이 된다. override는 부모 클래스에서 가져와 사용하겠다라고 명시하기 때문에 오타가 발생할 경우 (ex. PayMoeny라고 작성하는 경우) 컴파일 과정에서 에러가 발생해 우리가 알 수 있게 한다.
virtual 키워드는 객체지향 중 상속에 관련된 역할을 수행하는 것에 초점이 맞춰져 있다. 부모에서 선언함으로써 자식에서 제대로 사용할 수 있도록 선언해주는 것이기에 상속 후 자식에서도 재사용을 원하는 함수인 경우에는 virtual을 붙여 자식에서도 사용할 수 있도록 처리해주면 도움이 된다.