함수를 만들 때 사용할 수 있는 키워드를 알아보자. 이 키워드들은 함수를 만든 목적과 사용 방법을 명확히 함으로써 개발자 본인은 물론이고 공동 작업자나 라이브러리 사용자가 정확히 이해하고 사용할 수 있도록 돕는다.
함수의 목적과 사용 방법을 알리는 가장 확실한 방법은 잘못 사용했을 때 컴파일되지 않도록 하는 것이다.
delete
, default
, override
, final
그리고 noexcept
키워드를 이용하면 함수의 목적을 쉽게 파악할 수 있게 하고 잘못된 사용을 문법적으로 막을 수 있다.
default
키워드는 컴파일러가 제공하는 기본 함수를 사용하겠다는 의미이다. default
키워드를 사용하면 생성자의 경우, 매개변수가 없는 기본 생성자가 호출되고, 연산자는 오버로딩되지 않은 기본 연산자가 호출된다.
생성자를 예로 들어보자. 개발자가 클래스를 만들 때 생성자를 작성하지 않으면 매개변수가 없는 기본 생성자가 자동으로 생성된다. 하지만 매개변수를 포함하는 생성자를 작성하면 이 생성자만 사용할 수 있다. 즉, 생성자를 만들었으므로 매개변수가 없는 생성자는 자동으로 생성되지 않는다.
그런데 자동으로 생성된 기본 생성자를 사용하다가 매개변수를 포함하는 생성자를 추가한다면 기본 생성자를 사용할 수 없게 되므로 기본 생성자가 호출되는 코드에 문제가 발생한다. 또는 컴파일러가 만들어 주는 기본 생성자와 매개변수를 포함하는 생성자를 모두 사용하고 싶을 수 있다.
이럴 때에 default
키워드를 사용할 수 있다.
default
키워드는 컴파일러가 자동으로 만들어 주는 매개변수가 없는 기본 생성자를 사용하겠다고 알리는 방법이다.
#include <iostream>
#include <compare>
using std::cout;
using std::endl;
class monster_a {
public:
// 컴파일러 또는 언어가 제공하는 기본 생성자 사용
monster_a() = default;
monster_a(int init_hp, int init_power)
: hp(init_hp), power(init_power) {
}
int hp, power;
};
class monster_b {
public:
// 아무런 연산도 하지 않는 기본 생성자 정의
monster_b() {}
monster_b(int init_hp, int init_power)
: hp(init_hp), power(init_power) {
}
int hp, power;
};
class monster_c {
public:
// 매개변수 없는 기본 생성자 정의하지 않음
monster_c(int init_hp, int init_power)
: hp(init_hp), power(init_power) {
}
int hp, power;
};
int main()
{
// 기본 생성자로 객체 생성
monster_a mon_a{};
// 아무런 연산도 하지 않는 기본 생성자로 객체 생성
monster_b mon_b{};
// monster_c에 매개변수가 없는 기본 생성자가 없어, 객체 생성에 실패하여 컴파일 에러 발생
//monster_c mon_c{};
cout << "monster_a: " << mon_a.hp << ", " << mon_a.power << endl;
cout << "monster_b: " << mon_b.hp << ", " << mon_b.power << endl;
return 0;
}
실행 결과
monster_a: 0, 0
monster_b: -858993460, -858993460
monster_c
클래스에는 매개변수 없는 기본 생성자가 정의되지 않아 객체 생성이 안된다. monster_b
클래스에는 아무런 연산도 하지 않는 기본 생성자를 직접 정의했다. 얼핏 보면 차이가 없어 보이지만 그렇지 않다.
객체를 생성할 때 monster_a mon_a{}
처럼 중괄호 ({})
를 사용하면 C++11
에서 추가된 멤버 변수를 초기화하는 기본 생성자
를 호출한다. 실행 결과를 보면 기본 생성자로 만든 monster_a
의 객체는 정수형 멤버 변수값이 0
으로 초기화되었지만, 아무런 연산도 하지 않는 기본 생성자로 만든 monster_b
의 객체는 쓰레기 값
이 입력된 것을 알 수 있다.
delete
키워드는 더 이상 사용하지 않는 함수에 붙이는 키워드이다. 앞에서 함수의 목적과 사용 방법을 알리는 가장 확실한 방법은 잘못 사용했을 때 컴파일되지 않도록 하는 것이라고 했다. 문법이 틀리면 컴파일 오류가 나는 것처럼 개발 의도와 다르게 사용할 때 컴파일 오류가 발생하도록 하는 것이다.
소스 코드에서 함수가 변경되거나 삭제되면 호출 오류가 발생한다. 매개변수나 함수 이름의 변경은 매개변수 구성이 바뀌는 것이므로 그나마 쉽게 이해할 수 있지만, 삭제된 함수는 그 의도를 파악하기가 어렵다. 삭제된 것이 아니라 함수 이름이 바뀌거나 실수로 누락된 것일 수도 있기 때문이다.
이때 함수가 더 이상 사용되지 않고 삭제되었음을 알려 주는 키워드가 바로 delete
이다.
다음 코드는 delete
키워드를 사용한 경우와 그냥 삭제한 함수를 호출했을 때 오류 메시지를 비교한 예이다.
#include <iostream>
using std::cout;
using std::endl;
class monster {
public:
void create_monster() = delete;
// delete 키워드를 사용하지 않고 제거
//void create_monster_() {};
};
int main()
{
monster monster_sample;
monster_sample.create_monster();
//monster_sample.create_monster_();
return 0;
}
실행 결과
note: 'void monster::create_monster(void)': function was explicitly deleted
두 경우에 대한 오류 메시지가 다를 것이다. delete
키워드를 사용했을 때는 삭제 된 함수를 참조할 수 없다고 명확한 메시지를 보여준다. 하지만 delete
키워드를 사용하지 않고 그냥 삭제한 함수는 멤버가 없다는 오류가 발생한다.
이처럼 함수가 존재하지 않을 때와 delete
로 표기했을 때 오류가 다른 것을 프로그램을 개발할 때에 활용할 수 있다.
함수의 시그니처나 이름을 변경할 때 기존 함수에
delete
키워드를 붙이면, 변경 전 함수를 사용하던 개발자가 컴파일 오류를 통해서 사용 중인 함수가 제거되었음을 알 수 있다.
가상 함수
는 객체지향 언어의 주요 특징인 다형성
을 C++
언어에서 구현할 수 있도록 해주는 문법이다. 그런데 클래스 상속이 반복되다 보면 가상 함수 재정의를 잘못하는 실수를 할 수 있다.
실수로 다른 이름이나 다른 매개변수를 사용하는 것인지, 가상 함수를 상속받는 과정에서 발생한 실수인지 컴파일러는 명확하게 구별하지 못할 수 있다.
이럴 때, override
키워드를 사용할 수 있다. override
키워드를 사용해 가상 함수
를 상속받았음을 명확히 하는 것이다. 이때 가상 함수 상속 규칙에 어긋나는 오류가 있다면 컴파일 오류가 발생한다.
즉, 오버라이딩인지 아닌지를 컴파일러가 판단할 수 없을 때 판단할 수 있는 기준을 제시해 주는 것이다.
만약 더 이상 재정의하지 않도록 지정하고 싶다면 어떻게 해야 할까? 특ㅈ덩 시점에 가상 함수의 재정의나 클래스, 구조체의 상속을 막으려면 최종 상속 단계에서 final
키워드를 추가하면 된다.
final
키워드가 추가된 가상 함수나 클래스 등은 더 이상 상속할 수 없다. 만약 재정의나 상속을 시도하면 컴파일 오류가 발생하여 상속할 수 없음을 알려 준다. 이로써 제공자의 의도와 다르게 가상 함수가 변경되는 것을 막을 수 있다. 다만 클래스나 가상 함수는 final
키워드로 재정의나 상속을 막을 수 있지만, 일반 함수에는 사용할 수 없으니 주의해야 한다.
override
, final
사용법은 간단하다. 오버라이드로 재정의한 함수의 선언부 가장 마지막에 해당 키워드를 적어 주면 된다.
// 재정의된 가상 함수임을 알림
virtual function(...) override {};
// 재정의할 수 없음을 알림
virtual function(...) final {};
// 클래스를 더 이상 상속할 수 없음을 알림
class derivate_class final : base_class {};
// 구조체를 더 이상 상속할 수 없음을 알림
struct derivate_struct final : base_struct {};