네, C++의 가상 함수(virtual function)에도 기본 매개변수(디폴트 인자)를 선언할 수 있습니다. 다만 그 동작 방식에는 직관적이지 않은 부분이 있어 주의가 필요합니다. 아래 예제를 통해 자세히 살펴보겠습니다.
예제 코드
#include<iostream>
using namespace std;
class B {
public:
virtual void s(int a = 0) {
cout << " In Base \n";
}
};
class D : public B {
public:
virtual void s(int a) {
cout << "In Derived, a=" << a;
}
};
int main(void) {
D d; // D 클래스의 객체
B *b = &d; // d를 가리키는 B* 타입 포인터
b->s(); // 출력: In Derived, a=0
return 0;
}실행 결과
In Derived, a=0
결과 분석
실행 결과에서 눈여겨볼 점은 두 가지입니다. 첫째, 실제로 호출된 함수는 파생 클래스 D의 s()입니다. 둘째, 적용된 기본값은 기본 클래스 B의 s()에 선언된 a = 0이라는 점입니다. 즉, 호출되는 함수는 런타임에 결정되지만, 기본 인자 값은 컴파일 타임에 결정됩니다.
기본 인자는 함수 시그니처에 포함되지 않습니다
C++에서 기본 인자(default argument)는 함수 시그니처(signature)의 일부로 취급되지 않습니다. 따라서 기본 클래스의 s(int a = 0)와 파생 클래스의 s(int a)는 동일한 시그니처를 가진 것으로 간주되며, 파생 클래스의 s()는 기본 클래스의 s()를 정상적으로 오버라이드합니다.
컴파일러는 함수 호출에서 인자가 생략된 것을 발견하면 해당 시점에 기본값을 대신 삽입합니다. 위 프로그램에서 b->s() 호출 시 인자가 없으므로, 컴파일러는 포인터 b의 정적 타입(B*)을 기준으로 B::s()의 기본값 0을 코드에 밀어 넣습니다. 이후 런타임에는 가상 디스패치에 의해 실제 객체의 타입(D)에 맞는 D::s()가 호출되면서, 컴파일 타임에 삽입된 값 0이 그대로 전달되는 것입니다.
주의 사항 및 권장 사항
이처럼 가상 함수의 기본 인자는 정적 바인딩(static binding)으로 처리되기 때문에, 기본 클래스와 파생 클래스에서 서로 다른 기본값을 지정하면 개발자의 의도와 다르게 동작할 수 있습니다. 예를 들어 파생 클래스에서 s(int a = 10)으로 기본값을 변경하더라도, B* 포인터를 통해 호출하면 여전히 0이 전달됩니다.
따라서 C++ 코어 가이드라인(C++ Core Guidelines)에서도 가상 함수에 기본 인자를 사용하지 말 것을 권장합니다. 기본값이 필요하다면 함수 오버로딩을 활용하거나, 가상 함수를 호출하는 비가상 래퍼 함수(NVI 패턴)에서 기본 인자를 제공하는 방식이 훨씬 안전합니다.