C++ override와 final | "가상 함수" 가이드
이 글의 핵심
override로 오버라이드를 검증하고, final로 설계를 닫으며, 성능 관점까지 짚습니다.
override란?
override 는 C++11에서 도입된 키워드로, 가상 함수를 오버라이드한다는 것을 명시적으로 표시합니다. 컴파일러가 실제로 기반 클래스의 가상 함수를 오버라이드하는지 검증하여, 오타나 시그니처 불일치로 인한 버그를 방지합니다.
class Base {
public:
virtual void func() {
std::cout << "Base::func" << std::endl;
}
};
class Derived : public Base {
public:
// override 명시
void func() override {
std::cout << "Derived::func" << std::endl;
}
};
왜 필요한가?:
- 오타 방지: 함수 이름 오타 시 컴파일 에러
- 시그니처 검증: 매개변수나 const 불일치 감지
- 명확한 의도: 코드 리더에게 오버라이드 의도 전달
- 리팩토링 안전: 기반 클래스 변경 시 파생 클래스 오류 감지
class Base {
public:
virtual void process(int x) {}
};
class Derived : public Base {
public:
// ❌ override 없음: 오타 발견 못함
void proccess(int x) {} // 새 함수 생성 (버그!)
// ✅ override 사용: 컴파일 에러
void proccess(int x) override {} // 에러: 오버라이드할 함수 없음
};
override의 동작 원리:
override는 컴파일러에게 “이 함수는 기반 클래스의 가상 함수를 오버라이드한다”고 알립니다. 컴파일러는 다음을 검증합니다:
- 기반 클래스에 같은 이름의 가상 함수가 있는지
- 시그니처(매개변수, const, 반환 타입)가 일치하는지
- 기반 클래스 함수가
final이 아닌지
class Base {
public:
virtual void func(int x) const {}
};
class Derived : public Base {
public:
// 모두 컴파일 에러
// void func(int x) override {} // 에러: const 누락
// void func(double x) const override {} // 에러: 매개변수 타입 다름
// int func(int x) const override {} // 에러: 반환 타입 다름 (공변 반환 제외)
// ✅ 올바른 오버라이드
void func(int x) const override {}
};
가상 함수 오버라이드란 무엇인가
기반 클래스에 **virtual**이 붙은 멤버 함수는 동적 디스패치 대상입니다. 파생 클래스에서 같은 시그니처로 선언하면, 기반 포인터/참조를 통해 호출할 때 파생 구현이 실행됩니다. 이것이 오버라이드입니다.
- vtable: 대부분의 구현에서 가상 함수는 함수 포인터 테이블을 통해 간접 호출됩니다.
- 시그니처 일치: 이름·매개변수·
const·참조 한정자·일부 반환 타입(공변 반환)까지 맞아야 같은 슬롯을 덮어씁니다. 하나라도 다르면 새 함수가 될 수 있어 버그가 납니다.
struct Base {
virtual void f(int) const {}
virtual ~Base() = default;
};
struct Derived : Base {
void f(int) const override {} // Base::f를 오버라이드
};
override 키워드가 중요한 이유
override는 컴파일러에게 검증을 맡기는 표식입니다. 없으면 다음 실수가 조용히 통과합니다.
- 이름 오타:
draw대신drwa— 새 함수가 생김. const누락: 기반은void f() const, 파생은void f()— 오버라이드 아님.- 매개변수 타입 변경:
int→size_t등 — 또 다른 새 함수. - 기반 쪽 변경: 기반에서
virtual이 빠지거나 시그니처가 바뀌면, 파생은 전혀 다른 함수를 들고 있을 수 있음.
override를 붙이면 위 경우 즉시 컴파일 에러가 나므로, 다형성 계약이 깨지는 것을 빌드 단계에서 막습니다.
override의 장점
class Base {
public:
virtual void func(int x) {}
};
class Derived : public Base {
public:
// ❌ 오타 (새 함수 생성)
void fucn(int x) {} // 오타!
// ✅ override (컴파일 에러)
void fucn(int x) override {} // 에러: 오버라이드할 함수 없음
};
final이란?
final 은 C++11에서 도입된 키워드로, 상속이나 오버라이드를 금지합니다. 클래스에 사용하면 더 이상 상속할 수 없고, 가상 함수에 사용하면 더 이상 오버라이드할 수 없습니다.
// 클래스 final: 상속 금지
class FinalClass final {
public:
void func() {}
};
// class Derived : public FinalClass {}; // 에러
// 함수 final: 오버라이드 금지
class Base {
public:
virtual void func() final {}
};
class Derived : public Base {
public:
// void func() override {} // 에러: final 함수
};
왜 필요한가?:
- 설계 의도 명시: 더 이상 확장하지 않을 클래스/함수 표시
- 보안: 중요한 로직이 변경되지 않도록 보호
- 성능 최적화: 컴파일러가 devirtualization 수행 가능
- 안정성: 예상치 못한 상속으로 인한 버그 방지
// ❌ final 없음: 의도하지 않은 상속 가능
class SecurityManager {
public:
virtual void authenticate() {
// 중요한 보안 로직
}
};
class HackedManager : public SecurityManager {
public:
void authenticate() override {
// 보안 우회!
}
};
// ✅ final 사용: 상속 금지
class SecurityManager final {
public:
void authenticate() {
// 중요한 보안 로직 (안전)
}
};
// class HackedManager : public SecurityManager {}; // 에러
final의 성능 이점:
final을 사용하면 컴파일러가 devirtualization을 수행할 수 있습니다. 가상 함수 호출을 직접 호출로 변환하여 vtable 조회 오버헤드를 제거합니다.
class Base {
public:
virtual void func() final {
// 더 이상 오버라이드 없음
// 컴파일러가 직접 호출로 최적화 가능
}
};
// 호출 시
Base* ptr = new Base();
ptr->func(); // 직접 호출로 최적화 가능 (vtable 조회 불필요)
final 클래스 vs final 함수:
| 특징 | final 클래스 | final 함수 |
|---|---|---|
| 적용 대상 | 클래스 전체 | 특정 가상 함수 |
| 효과 | 상속 금지 | 오버라이드 금지 |
| 사용 위치 | 클래스 선언 | 가상 함수 선언 |
| 성능 | 모든 가상 함수 최적화 | 해당 함수만 최적화 |
// final 클래스
class FinalClass final {
virtual void func1() {}
virtual void func2() {}
};
// final 함수
class Base {
virtual void func1() final {} // 이 함수만 final
virtual void func2() {} // 오버라이드 가능
};
final 클래스 vs final 함수 — 선택 가이드
| 구분 | 의미 | 쓰는 때 |
|---|---|---|
class D final | D를 상속할 수 없음 | 타입 계층을 여기서 끝낸다고 확정할 때(보안·성능·불변식). |
virtual void f() final | f는 파생에서 더 이상 오버라이드 불가 | 파이프라인의 한 단계만 고정하고 나머지 훅은 열어둘 때. |
final 클래스는 해당 클래스의 모든 가상 호출에 대해 “이 아래로는 더 이상 파생이 없다”는 정보를 컴파일러에 줄 수 있어 최적화(아래 ‘성능’ 참고)에 유리한 경우가 있습니다. final 함수는 한 메서드만 닫을 때 씁니다. 확장 가능성을 나중에 열어야 할지 불확실하면 남용하지 않는 것이 좋습니다.
실전 예시
예시 1: 도형 클래스
class Shape {
public:
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};
class Rectangle final : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override final {
return width * height;
}
void draw() const override {
std::cout << "Drawing Rectangle" << std::endl;
}
};
// class Square : public Rectangle {}; // 에러: final 클래스
예시 2: 로거 계층
class Logger {
public:
virtual void log(const std::string& message) {
std::cout << "[LOG] " << message << std::endl;
}
virtual ~Logger() = default;
};
class FileLogger : public Logger {
private:
std::ofstream file;
public:
FileLogger(const std::string& filename) : file(filename) {}
void log(const std::string& message) override {
file << "[FILE] " << message << std::endl;
}
};
class SecureLogger final : public FileLogger {
public:
SecureLogger(const std::string& filename)
: FileLogger(filename) {}
void log(const std::string& message) override final {
// 암호화 후 로깅
FileLogger::log("ENCRYPTED: " + message);
}
};
예시 3: 템플릿 메서드 패턴
class GameCharacter {
public:
void takeDamage(int damage) {
beforeDamage();
applyDamage(damage);
afterDamage();
}
virtual ~GameCharacter() = default;
protected:
virtual void beforeDamage() {}
virtual void applyDamage(int damage) = 0;
virtual void afterDamage() {}
};
class Warrior : public GameCharacter {
private:
int health = 100;
int armor = 50;
protected:
void beforeDamage() override {
std::cout << "방어 자세" << std::endl;
}
void applyDamage(int damage) override final {
int actualDamage = std::max(0, damage - armor);
health -= actualDamage;
std::cout << "데미지: " << actualDamage << std::endl;
}
void afterDamage() override {
if (health <= 0) {
std::cout << "전사 사망" << std::endl;
}
}
};
예시 4: 인터페이스 구현
class IDatabase {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual void query(const std::string& sql) = 0;
virtual ~IDatabase() = default;
};
class MySQLDatabase : public IDatabase {
public:
void connect() override {
std::cout << "MySQL 연결" << std::endl;
}
void disconnect() override {
std::cout << "MySQL 연결 해제" << std::endl;
}
void query(const std::string& sql) override {
std::cout << "MySQL 쿼리: " << sql << std::endl;
}
};
class PostgreSQLDatabase final : public IDatabase {
public:
void connect() override {
std::cout << "PostgreSQL 연결" << std::endl;
}
void disconnect() override {
std::cout << "PostgreSQL 연결 해제" << std::endl;
}
void query(const std::string& sql) override final {
std::cout << "PostgreSQL 쿼리: " << sql << std::endl;
}
};
override와 const
class Base {
public:
virtual void func() const {}
};
class Derived : public Base {
public:
// ❌ const 누락 (새 함수)
void func() {}
// ✅ override (에러 발견)
void func() override {} // 에러: const 불일치
// ✅ 올바른 오버라이드
void func() const override {}
};
자주 발생하는 문제
문제 1: 오타
class Base {
public:
virtual void process() {}
};
class Derived : public Base {
public:
// ❌ 오타 (새 함수 생성)
void proccess() {} // 오타!
// ✅ override (에러 발견)
void proccess() override {} // 에러
};
문제 2: 시그니처 불일치
class Base {
public:
virtual void func(int x) {}
};
class Derived : public Base {
public:
// ❌ 매개변수 타입 다름
void func(double x) {}
// ✅ override (에러 발견)
void func(double x) override {} // 에러
};
문제 3: final 상속 시도
class Base final {
public:
void func() {}
};
// ❌ final 클래스 상속
// class Derived : public Base {}; // 에러
// ✅ 컴포지션 사용
class Derived {
private:
Base base;
public:
void func() {
base.func();
}
};
문제 4: final 함수 오버라이드
class Base {
public:
virtual void func() final {}
};
class Derived : public Base {
public:
// ❌ final 함수 오버라이드
// void func() override {} // 에러
};
override와 final 조합
class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
class Middle : public Base {
public:
void func1() override {
// 오버라이드만
}
void func2() override final {
// 오버라이드하고 더 이상 오버라이드 금지
}
};
class Derived : public Middle {
public:
void func1() override {
// OK
}
// void func2() override {} // 에러: final
};
성능 고려사항
// final 클래스: devirtualization 가능
class FinalClass final {
public:
virtual void func() {
// 컴파일러가 직접 호출로 최적화 가능
}
};
// final 함수: 인라인 가능
class Base {
public:
virtual void func() final {
// 더 이상 오버라이드 없음 → 인라인 가능
}
};
성능 영향: vtable, devirtualization, 측정 시 유의점
- 일반적인 가상 호출: 정적 분석만으로는 대상 함수가 확정되지 않으면 간접 분기(vtable 경유)가 됩니다. 나노초 단위라도 핫 루프에서는 누적될 수 있습니다.
final/final class: 파생이 없다는 사실이 알려지면 컴파일러는 직접 호출로 바꾸거나 인라인할 여지가 커집니다(구현·최적화 수준 의존, UB 없이 가능한 경우).override자체: 컴파일 타임 표식이라 런타임 비용은 없습니다. 오히려 잘못된 오버라이드를 막아 불필요한 가상 호출을 줄이는 데 도움이 됩니다.
실무: final을 성능 때문에 붙이기 전에 프로파일로 병목이 가상 호출인지 확인하는 것이 안전합니다. API를 닫는 것은 설계·유지보수와의 트레이드오프가 더 큽니다.
실무 패턴
패턴 1: 인터페이스 + final 구현
// 인터페이스 (순수 가상 함수)
class IPaymentProcessor {
public:
virtual void processPayment(double amount) = 0;
virtual bool validateCard(const std::string& cardNumber) = 0;
virtual ~IPaymentProcessor() = default;
};
// final 구현 (더 이상 확장 불필요)
class SecurePaymentProcessor final : public IPaymentProcessor {
public:
void processPayment(double amount) override {
if (validateCard(currentCard_)) {
// 결제 처리
std::cout << "결제 완료: $" << amount << '\n';
}
}
bool validateCard(const std::string& cardNumber) override final {
// 보안 검증 로직 (변경 금지)
return cardNumber.length() == 16;
}
private:
std::string currentCard_;
};
패턴 2: 템플릿 메서드 + final
class DataProcessor {
public:
// 템플릿 메서드 (변경 금지)
void process() final {
loadData();
validateData();
transformData();
saveData();
}
virtual ~DataProcessor() = default;
protected:
virtual void loadData() = 0;
virtual void validateData() = 0;
virtual void transformData() = 0;
virtual void saveData() = 0;
};
class CSVProcessor : public DataProcessor {
protected:
void loadData() override {
std::cout << "CSV 로드\n";
}
void validateData() override {
std::cout << "CSV 검증\n";
}
void transformData() override {
std::cout << "CSV 변환\n";
}
void saveData() override {
std::cout << "CSV 저장\n";
}
};
패턴 3: 계층적 final
class Base {
public:
virtual void step1() {}
virtual void step2() {}
virtual void step3() {}
virtual ~Base() = default;
};
class Middle : public Base {
public:
void step1() override final {
// step1은 더 이상 오버라이드 불가
}
void step2() override {
// step2는 계속 오버라이드 가능
}
};
class Derived : public Middle {
public:
// void step1() override {} // 에러: final
void step2() override {
// OK
}
void step3() override {
// OK
}
};
FAQ
Q1: override는 언제 사용해야 하나요?
A: 가상 함수를 오버라이드할 때 항상 사용하세요. 오타, 시그니처 불일치, const 누락 등을 컴파일 타임에 감지합니다.
class Base {
public:
virtual void process(int x) const {}
};
class Derived : public Base {
public:
// ✅ override 사용: 모든 오류 감지
void process(int x) const override {}
// ❌ override 없음: 오류 발견 못함
// void proccess(int x) const {} // 오타
// void process(int x) {} // const 누락
// void process(double x) const {} // 타입 다름
};
Q2: final은 언제 사용해야 하나요?
A:
- 더 이상 상속/오버라이드가 불필요할 때: 설계가 완성됨
- 보안/안정성이 중요할 때: 중요한 로직 보호
- 성능 최적화가 필요할 때: devirtualization 활용
// 보안: 인증 로직 보호
class AuthenticationManager final {
// 더 이상 상속 불가
};
// 성능: 최적화 가능
class HighPerformanceClass {
public:
virtual void criticalPath() final {
// devirtualization 가능
}
};
Q3: override 없이도 동작하나요?
A: 네, 동작합니다. 하지만 override를 사용하는 것이 강력히 권장됩니다. 안전성과 명확성을 크게 향상시킵니다.
class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
// 동작하지만 권장하지 않음
void func() {}
// 권장: override 사용
void func() override {}
};
Q4: final의 성능 이점은?
A: devirtualization과 인라인 최적화가 가능합니다. 가상 함수 호출 오버헤드(vtable 조회)를 제거할 수 있습니다.
class Base {
public:
virtual void func() final {
// 컴파일러가 직접 호출로 최적화 가능
}
};
// 호출 시
Base* ptr = new Base();
ptr->func(); // vtable 조회 없이 직접 호출 가능
성능 비교:
- 일반 가상 함수: vtable 조회 + 간접 호출 (~5-10ns)
- final 함수: 직접 호출 + 인라인 가능 (~0ns)
Q5: override와 virtual의 차이는?
A:
virtual: 기반 클래스에서 가상 함수 선언override: 파생 클래스에서 오버라이드 명시
class Base {
public:
virtual void func() {} // virtual: 가상 함수 선언
};
class Derived : public Base {
public:
void func() override {} // override: 오버라이드 명시
};
함께 사용할 수 없음: 파생 클래스에서 virtual override는 불필요합니다.
Q6: override와 final을 함께 사용할 수 있나요?
A: 예, 가능합니다. 기반 클래스 함수를 오버라이드하면서 동시에 더 이상의 오버라이드를 금지합니다.
class Base {
public:
virtual void func() {}
};
class Middle : public Base {
public:
void func() override final {
// 오버라이드하고 더 이상 오버라이드 금지
}
};
class Derived : public Middle {
public:
// void func() override {} // 에러: final
};
Q7: final 클래스를 사용하면 어떤 단점이 있나요?
A: 확장성 제한이 주요 단점입니다. 나중에 기능을 확장하고 싶어도 불가능합니다.
class FinalClass final {
// 좋은 설계였지만...
};
// 나중에 확장이 필요해도 불가능
// class ExtendedClass : public FinalClass {}; // 에러
권장: 정말 확실할 때만 final 사용. 불확실하면 final 없이 설계.
Q8: override/final 학습 리소스는?
A:
- “Effective Modern C++” (Item 12: Declare overriding functions override) by Scott Meyers
- cppreference.com - override specifier
- cppreference.com - final specifier
- “C++ Primer” (5th Edition) by Stanley Lippman
관련 글: Virtual Functions, Inheritance.
한 줄 요약: override는 가상 함수 오버라이드를 명시하고, final은 상속/오버라이드를 금지하는 C++11 키워드입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 상속과 다형성 | “virtual 함수” 완벽 가이드
- C++ noexcept | “예외 없음 지정” 가이드
- C++ ratio | “컴파일 타임 분수” 가이드
관련 글
- C++ async & launch |
- C++ Atomic Operations |
- C++ Attributes |
- C++ auto 키워드 |
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기