C++ override와 final | "가상 함수" 가이드

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는 컴파일러에게 “이 함수는 기반 클래스의 가상 함수를 오버라이드한다”고 알립니다. 컴파일러는 다음을 검증합니다:

  1. 기반 클래스에 같은 이름의 가상 함수가 있는지
  2. 시그니처(매개변수, const, 반환 타입)가 일치하는지
  3. 기반 클래스 함수가 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컴파일러에게 검증을 맡기는 표식입니다. 없으면 다음 실수가 조용히 통과합니다.

  1. 이름 오타: draw 대신 drwa — 새 함수가 생김.
  2. const 누락: 기반은 void f() const, 파생은 void f() — 오버라이드 아님.
  3. 매개변수 타입 변경: intsize_t 등 — 또 다른 새 함수.
  4. 기반 쪽 변경: 기반에서 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 finalD를 상속할 수 없음타입 계층을 여기서 끝낸다고 확정할 때(보안·성능·불변식).
virtual void f() finalf는 파생에서 더 이상 오버라이드 불가파이프라인의 한 단계만 고정하고 나머지 훅은 열어둘 때.

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:

관련 글: 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 타입 추론 | 복잡한 타입을 컴파일러에 맡기기