Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 8 – Inheritance and polymorphism

Abstract classes and interfaces in C++

Define C++ interfaces with pure virtual functions, keep shared checks in one place with the non-virtual interface idiom, and test code with fakes.

  • Intermediate
  • 15 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define an interface as a class with pure virtual functions and a virtual destructor
  • Explain why an abstract class cannot be instantiated and what a derived class must override
  • Test code that depends on an interface by giving it a fake implementation
  • Apply the non-virtual interface idiom to keep checks and logging in one place
  • Predict what happens when a constructor reaches a pure virtual function

Before you start

What you’ll learn · Before you start

What you will learn

  • Define an interface as a class with pure virtual functions and a virtual destructor
  • Explain why an abstract class cannot be instantiated and what a derived class must override
  • Test code that depends on an interface by giving it a fake implementation
  • Apply the non-virtual interface idiom to keep checks and logging in one place
  • Predict what happens when a constructor reaches a pure virtual function

Before you start

On this page

The Checkout of this lesson charges 2,499.00 to a card and 999.00 to a wallet, and its five tests run against a fake that records every charge without touching either. It can do both because it depends on nothing but an interface: In C++, a class with only pure virtual functions and a public virtual destructor, and no data: it says what a type can do and leaves how to the classes that implement it. C++ has no interface keyword; this is a convention.: a class of pure virtual functions that says what a payment method can do and leaves how to each class that implements it.

You will make a class abstract with pure virtual functions, build that interface and the fake that tests the code using it, keep checks and logging in one place with the non-virtual interface idiom, and see what happens when a pure virtual function is called anyway. Every example runs on Apple clang 21 and GCC 16.

Pure virtual functions make a class abstract

An abstract class: A class with at least one pure virtual function that it has not overridden. No object of it can be created, not even as a by-value parameter, but references and pointers to it can refer to objects of its concrete derived classes. is a class with at least one pure virtual function: A virtual function declared with = 0 instead of a body, as in virtual std::string name() const = 0;. It makes its class abstract, and each derived class must override it before objects of that class can be created., a virtual function declared with = 0 instead of a body: virtual std::string name() const = 0;. It states what every derived class must be able to do without saying how, and the compiler refuses to create an object of it. A derived class becomes usable, or concrete, once it overrides every pure virtual function it inherits. payment.h declares the interface PaymentMethod and a Checkout that uses nothing else:

payment.h C header · payment.h
// An interface for ways to pay, and a checkout that only knows the interface.
#pragma once
#include <cstdint>
#include <format>
#include <string>

using Money = std::int64_t;  // in the smallest unit of the currency (paise, cents)

// Every way to pay can charge an amount and say what it is called. How it does that is up to each class.
class PaymentMethod {
public:
    virtual ~PaymentMethod() = default;
    // Charges amount for an order; returns a transaction id, or an empty string when the payment is declined.
    virtual std::string charge(Money amount, const std::string& order_id) = 0;
    virtual std::string name() const = 0;
};

// The checkout works with any PaymentMethod: it never names a concrete class.
class Checkout {
public:
    explicit Checkout(PaymentMethod& method) : method_(method) {}

    std::string pay(const std::string& order_id, Money amount) {
        if (amount <= 0) return std::format("{}: nothing to pay", order_id);
        const std::string transaction = method_.charge(amount, order_id);
        if (transaction.empty()) return std::format("{}: {} declined", order_id, method_.name());
        return std::format("{}: paid {}.{:02} by {} ({})", order_id, amount / 100, amount % 100, method_.name(),
                           transaction);
    }

private:
    PaymentMethod& method_;
};

PaymentMethod has no data and no code, only promises: anything that is a PaymentMethod can charge() an amount and report its name(). Because it is abstract, neither the interface itself nor a class that keeps one of its pure virtual functions can be created:

abstract_errors.cpp C++ · abstract_errors.cpp
// Neither the interface nor a class that overrides only part of it can be created.
#include <string>

#include "payment.h"

class GiftCard : public PaymentMethod {
public:
    std::string charge(Money, const std::string&) override { return "gift"; }
    // name() is not overridden, so GiftCard is still abstract
};

int main() {
    PaymentMethod method;
    GiftCard card;
}
Two objects that cannot exist Python · abstract_errors_session.py
from session import run

# abstract_errors.cpp with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra -c abstract_errors.cpp
g++-16 -std=c++23 -Wall -Wextra -c abstract_errors.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c abstract_errors.cpp
abstract_errors.cpp:13:19: error: variable type 'PaymentMethod' is an abstract class
   13 |     PaymentMethod method;
      |                   ^
./payment.h:14:25: note: unimplemented pure virtual method 'charge' in 'PaymentMethod'
   14 |     virtual std::string charge(Money amount, const std::string& order_id) = 0;
      |                         ^
./payment.h:15:25: note: unimplemented pure virtual method 'name' in 'PaymentMethod'
   15 |     virtual std::string name() const = 0;
      |                         ^
abstract_errors.cpp:14:14: error: variable type 'GiftCard' is an abstract class
   14 |     GiftCard card;
      |              ^
./payment.h:15:25: note: unimplemented pure virtual method 'name' in 'GiftCard'
   15 |     virtual std::string name() const = 0;
      |                         ^
2 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c abstract_errors.cpp
abstract_errors.cpp: In function 'int main()':
abstract_errors.cpp:13:19: error: cannot declare variable 'method' to be of abstract type 'PaymentMethod'
   13 |     PaymentMethod method;
      |                   ^~~~~~
In file included from abstract_errors.cpp:4:
payment.h:10:7: note: because the following virtual functions are pure within 'PaymentMethod':
   10 | class PaymentMethod {
      |       ^~~~~~~~~~~~~
  • 'virtual std::string PaymentMethod::charge(Money, const std::string&)'
    payment.h:14:25:
       14 |     virtual std::string charge(Money amount, const std::string& order_id) = 0;
          |                         ^~~~~~
  • 'virtual std::string PaymentMethod::name() const'
    payment.h:15:25:
       15 |     virtual std::string name() const = 0;
          |                         ^~~~
abstract_errors.cpp:14:14: error: cannot declare variable 'card' to be of abstract type 'GiftCard'
   14 |     GiftCard card;
      |              ^~~~
abstract_errors.cpp:6:7: note: because the following virtual functions are pure within 'GiftCard':
    6 | class GiftCard : public PaymentMethod {
      |       ^~~~~~~~
  • 'virtual std::string PaymentMethod::name() const'
    payment.h:15:25:
       15 |     virtual std::string name() const = 0;
          |                         ^~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 abstract_errors_session.py

Both compilers name every function that is still missing. GiftCard overrides charge() but not name(), so it inherits a pure virtual function and stays abstract. Follow the three classes of this lesson from abstract to concrete:

Which classes of this lesson can be created
Criterion Pure functions leftAn object of it
PaymentMethod, overrides nothing charge() and name()Error
GiftCard, overrides charge() name()Error
CardPayment, overrides both NoneCompiles

The rule reaches wherever an object of the class would have to exist. A function may even be declared with an abstract class as a by-value parameter or return type, but defining such a function does not compile, and neither does passing an object to such a parameter, because each would need an object of the abstract class. References and pointers to an abstract class are fine, and they are how an interface is used.

Checkpoint

Check: Pure virtual functions make a class abstract

  1. Question 1 of 2 GiftCard overrides charge() and nothing else, so GiftCard card; does not compile. What must it add to become concrete?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An override of std::string name() const

    A derived class becomes concrete once it overrides every pure virtual function it inherits. GiftCard still inherits the pure name() const, so it needs std::string name() const override.

  2. Question 2 of 2 card is a CardPayment, and PaymentMethod is abstract. Which lines compile?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • PaymentMethod& method = card;
    • PaymentMethod* method = &card;

    No object of an abstract class can exist, as a variable, a copy, a parameter or a result. References and pointers to it can, because they refer to an object of a concrete class.

An interface is a pure abstract class

Two real implementations, and a Checkout for each:

checkout.cpp C++ · checkout.cpp
// Two implementations of PaymentMethod, used by the same Checkout.
#include <format>
#include <print>
#include <string>
#include <utility>

#include "payment.h"

class CardPayment : public PaymentMethod {
public:
    explicit CardPayment(std::string last_digits) : last_digits_(std::move(last_digits)) {}
    std::string charge(Money amount, const std::string& order_id) override {
        // A real class would call the card network here. This one approves amounts up to 50,000.00.
        if (amount > 5'000'000) return "";
        return std::format("card-{}-{}", order_id, ++count_);
    }
    std::string name() const override { return "card ending " + last_digits_; }

private:
    std::string last_digits_;
    int count_ = 0;
};

class WalletPayment : public PaymentMethod {
public:
    explicit WalletPayment(Money balance) : balance_(balance) {}
    std::string charge(Money amount, const std::string& order_id) override {
        if (amount > balance_) return "";
        balance_ -= amount;
        return "wallet-" + order_id;
    }
    std::string name() const override { return std::format("wallet ({}.{:02} left)", balance_ / 100, balance_ % 100); }

private:
    Money balance_;
};

int main() {
    CardPayment card{"4242"};
    WalletPayment wallet{120'000};

    Checkout by_card{card};
    Checkout by_wallet{wallet};
    std::println("{}", by_card.pay("A-1001", 249'900));
    std::println("{}", by_wallet.pay("A-1002", 99'900));
    std::println("{}", by_wallet.pay("A-1003", 99'900));
}
One Checkout, two ways to pay Python · checkout_session.py
from session import run

# checkout.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra checkout.cpp -o checkout && ./checkout
g++-16 -std=c++23 -Wall -Wextra checkout.cpp -o checkout-gcc && ./checkout-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra checkout.cpp -o checkout && ./checkout
A-1001: paid 2499.00 by card ending 4242 (card-A-1001-1)
A-1002: paid 999.00 by wallet (201.00 left) (wallet-A-1002)
A-1003: wallet (201.00 left) declined
$ g++-16 -std=c++23 -Wall -Wextra checkout.cpp -o checkout-gcc && ./checkout-gcc
A-1001: paid 2499.00 by card ending 4242 (card-A-1001-1)
A-1002: paid 999.00 by wallet (201.00 left) (wallet-A-1002)
A-1003: wallet (201.00 left) declined

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checkout_session.py

Checkout was written without knowing that cards or wallets exist. It calls charge() and name() through its PaymentMethod&, and each call runs the override of the object it was given: the card approves 2,499.00, the wallet pays 999.00 and then declines the same amount, because only 201.00 is left. Step through the three payments:

Interactive · message by message Three payments through one PaymentMethod& Step through the three calls of pay() in main. Checkout talks only to its PaymentMethod&; the right two columns are the objects behind it.
Three payments through one PaymentMethod&, step 7 of 7. Messages so far: Checkout::pay() to card: charge(249900, A-1001); card to Checkout::pay(): card-A-1001-1; Checkout::pay() to card: name(): card ending 4242; Checkout::pay() to wallet: charge(99900, A-1002); wallet to Checkout::pay(): wallet-A-1002; Checkout::pay() to wallet: charge(99900, A-1003); wallet to Checkout::pay(): an empty string.Checkout::pay()holds a PaymentMethod&carda CardPayment, 4242walleta WalletPayment, 201.00charge(249900, A-1001)card-A-1001-1name(): card ending 4242charge(99900, A-1002)wallet-A-1002charge(99900, A-1003)an empty string

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. A-1001 by card. by_card.pay("A-1001", 249'900) has a positive amount, so Checkout calls charge() through its PaymentMethod&. The object behind it is the card.
  2. Approved. CardPayment::charge() approves amounts up to 50,000.00 and returns a transaction id: card-A-1001-1.
  3. Its name, the same way. Checkout asks name(), which runs CardPayment::name(), and prints A-1001: paid 2499.00 by card ending 4242 (card-A-1001-1).
  4. A-1002 by wallet. by_wallet is another Checkout, given the wallet. The same code calls charge(), and this time WalletPayment::charge() runs.
  5. Paid from the balance. The wallet holds 1,200.00, so it pays 999.00 and keeps 201.00: A-1002: paid 999.00 by wallet (201.00 left) (wallet-A-1002).
  6. A-1003 by wallet. The third order asks the wallet for 999.00 again. Only 201.00 is left, so charge() returns an empty string, which means declined.
  7. Declined. Checkout reports A-1003: wallet (201.00 left) declined. It never named CardPayment or WalletPayment: every call went through the interface.

The drawing shows the same design as dependencies: Checkout points at the interface only, and every implementation hangs below it.

Checkout calls only the PaymentMethod interface, which CardPayment, WalletPayment and the test FakePayment implement.Checkoutholds a PaymentMethod&PaymentMethod, an interface charge() = 0name() = 0virtual destructorno data membersthree implementationsCardPaymentin the programWalletPaymentin the programFakePaymentin the testscalls onlythese functionsimplemented by(each overrides both)

Code that depends on an interface works with every implementation

Text description of the diagram

The diagram shows the classes of payment.h, checkout.cpp and checkout_tests.cpp, from top to bottom.

  1. At the top, Checkout holds a reference to a PaymentMethod. An arrow labelled "calls only these functions" points down to PaymentMethod.
  2. PaymentMethod is an interface: it declares the pure virtual functions charge() and name(), each written with = 0, and a virtual destructor, and it has no data members.
  3. A dashed arrow labelled "implemented by, each overrides both" leads down to a box of three implementations: CardPayment and WalletPayment, used by the program, and FakePayment, used by the tests.
  4. Checkout never names any of the three, so each of them can be given to it without changing a line of Checkout.

A good interface in C++ looks like PaymentMethod:

  • Only pure virtual functions. Rule C.121 of the C++ Core Guidelines says that a base class used as an interface should be a pure abstract class, and rule I.25 prefers such empty classes to hierarchies that carry data.
  • A public virtual destructor, so that an implementation can be deleted through the interface, for example by a std::unique_ptr<PaymentMethod>. If you declare it pure, virtual ~PaymentMethod() = 0;, you must still define it outside the class, because the destructor of every derived class calls it.
  • No data members and no constructors. Data belongs to the implementations, which keeps the interface stable when they change.

Rule C.129 of the Guidelines names the distinction behind this: inheriting an interface promises what a class can do, while inheriting an implementation reuses code. An interface keeps the two apart, so a class can implement it without inheriting anything it did not ask for.

Payments in India

A UpiPayment class for UPI would be one more implementation of the same interface. Checkout and its tests would not change.

Checkpoint

Check: Interfaces

  1. Question 1 of 2 The wallet starts with 1,200.00. Why is order A-1003 declined?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: After A-1002 paid 999.00, only 201.00 was left, so WalletPayment::charge() refused 999.00

    Every call through PaymentMethod& runs the override of the object behind it. The wallet's charge() compares the amount with its balance, 201.00 after A-1002, and declines.

  2. Question 2 of 2 Which of these belong in an interface such as PaymentMethod?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • A public virtual destructor
    • Pure virtual functions

    An interface holds only pure virtual functions and a public virtual destructor. Everything else, data and code, belongs to the classes that implement it, which keeps the interface stable when they change.

A fake tests the code without the real thing

The real payment classes would contact a card network or a wallet service. A test does not need either, because Checkout only depends on the interface. A fake: A small implementation of an interface written for tests, such as a payment method that approves or declines on command and records every charge, so that a test can run the code without the real service and check how it was used. is a small implementation written for tests: this one approves or declines on command and records every charge it receives.

checkout_tests.cpp C++ · checkout_tests.cpp
// Tests for Checkout with a fake PaymentMethod: no card network, no wallet, and every call is recorded.
#include <print>
#include <string>
#include <vector>

#include "payment.h"

// A fake: it approves or declines as the test says, and remembers what it was asked to do.
class FakePayment : public PaymentMethod {
public:
    bool approve = true;
    std::vector<Money> charged;

    std::string charge(Money amount, const std::string& order_id) override {
        charged.push_back(amount);
        return approve ? "fake-" + order_id : "";
    }
    std::string name() const override { return "fake"; }
};

namespace {
int checks = 0;
int passed = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (ok) ++passed;
    else std::println("FAILED: {}", what);
}
}  // namespace

int main() {
    FakePayment fake;
    Checkout checkout{fake};

    check(checkout.pay("T-1", 12'345) == "T-1: paid 123.45 by fake (fake-T-1)", "an approved payment is reported");
    check(fake.charged == std::vector<Money>{12'345}, "the method is charged once, with the exact amount");

    fake.approve = false;
    check(checkout.pay("T-2", 500) == "T-2: fake declined", "a declined payment is reported");

    check(checkout.pay("T-3", 0) == "T-3: nothing to pay", "a zero amount needs no payment");
    check(fake.charged.size() == 2, "a zero amount never reaches the payment method");

    std::println("{} of {} checks passed", passed, checks);
    return passed == checks ? 0 : 1;
}
Five checks, no real payment Python · checkout_tests_session.py
from session import run

# The tests built with both compilers, the first build with the sanitizers on.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined checkout_tests.cpp -o checkout_tests && ./checkout_tests
g++-16 -std=c++23 -Wall -Wextra checkout_tests.cpp -o checkout_tests-gcc && ./checkout_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined checkout_tests.cpp -o checkout_tests && ./checkout_tests
5 of 5 checks passed
$ g++-16 -std=c++23 -Wall -Wextra checkout_tests.cpp -o checkout_tests-gcc && ./checkout_tests-gcc
5 of 5 checks passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checkout_tests_session.py

Step through the test and watch what the fake records at each payment:

Interactive · step by step Five checks of Checkout, and what the fake saw Step through the test. The left column is the test and the Checkout it drives, the right one the fake and the amounts it recorded.
Five checks of Checkout, and what the fake saw, step 5 of 5. Showing checkout_tests.cpp (5 of 5 checks so far), Checkout checkout{fake}, FakePayment fake (approve = false), fake.charged ({ 12345, 500 }). In focus: checkout_tests.cpp.pay()charge()recordscheckout_tests.cpp5 of 5 checks so farCheckout checkout{fake}holds a PaymentMethod&FakePayment fakeapprove = falsefake.charged{ 12345, 500 }

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. T-1, approved. The test pays 12,345, which is 123.45. Checkout calls charge() through its PaymentMethod&, and the object behind it is the fake, which records the amount and approves.
  2. Two checks pass. Checkout reports T-1: paid 123.45 by fake (fake-T-1), and the fake's record says it was charged once, with the exact amount: two checks pass.
  3. T-2, declined. The test sets fake.approve = false and pays 500. The fake records it and returns an empty string, so Checkout reports T-2: fake declined.
  4. T-3, nothing to pay. An order of 0 never reaches the payment method: Checkout returns T-3: nothing to pay without calling charge(), and the record still holds two amounts.
  5. No network at all. Both compilers printed 5 of 5 checks passed, the Clang build under both sanitizers without a report. No card network, no wallet, and the run takes no time.

The tests check more than return values. They check how Checkout used the interface: one charge per payment, the exact amount, and no charge at all for an order that costs nothing. A fake makes such questions easy to ask, and the run takes no time and needs no network. The first build also runs under AddressSanitizer and UndefinedBehaviorSanitizer, which reported nothing.

The cost of this design is small but real: every call through the interface is a virtual call, and each implementation is a separate class. For code where that cost matters, later modules show interfaces checked at compile time, with templates and concepts.

Key idea

Code that depends only on an interface works with every class that implements it, including a fake that a test controls and inspects, so the test needs neither the network nor the real service.

Checkpoint

Check: Testing through a fake

  1. Question 1 of 2 The test pays T-1 (12,345), then T-2 (500, declined) and then T-3 (0). How many amounts does fake.charged hold at the end?

    Type a number.

    A hint

    Which of the three orders reaches charge()?

    Show the answer: Question 1 of 2

    Answer: 2 amounts

    T-1 and T-2 call charge(), approved or not, so the fake records 12,345 and 500. T-3 costs nothing, and Checkout returns "nothing to pay" before it calls the method, which the last check confirms.

  2. Question 2 of 2 Suppose Checkout::pay() called charge() before it tested amount <= 0. Which check would then fail?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: a zero amount never reaches the payment method

    A fake lets a test check how the code used the interface, not only what it returned. The fake's record would show a charge of 0 that should never happen.

The non-virtual interface idiom keeps the checks in one place

Every exporter of a report should refuse an empty report and log each export the same way. If those steps live in each derived class, one of them will forget them. The non-virtual interface idiom (NVI): A design in which a base class's public function is not virtual: it runs the steps every implementation shares, such as checks and logging, and calls private virtual hooks that derived classes override for the part that differs. (NVI) puts them in the base class instead: the public function is not virtual and does the shared work, and it calls private virtual functions, the hooks, for the part that differs.

exporter.cpp C++ · exporter.cpp
// The non-virtual interface idiom: export_report() is public and not virtual. It checks the report and writes the
// log lines, once, for every format. Derived classes override only the private hooks.
#include <array>
#include <print>
#include <string>
#include <vector>

struct Report {
    std::string title;
    std::vector<int> values;
};

class Exporter {
public:
    virtual ~Exporter() = default;

    // The only way in: the same check and the same log lines for every format.
    bool export_report(const Report& report) const {
        if (report.values.empty()) {
            std::println("[{}] refused \"{}\": no values", format_name(), report.title);
            return false;
        }
        std::println("[{}] exporting \"{}\" ({} values)", format_name(), report.title, report.values.size());
        const std::string body = write_body(report);
        std::println("{}", body);
        std::println("[{}] done, {} bytes", format_name(), body.size());
        return true;
    }

private:
    // The hooks: each format decides what they do, but never when they run.
    virtual std::string format_name() const = 0;
    virtual std::string write_body(const Report& report) const = 0;
};

class CsvExporter : public Exporter {
private:
    std::string format_name() const override { return "csv"; }
    std::string write_body(const Report& report) const override {
        std::string out = "title,value";
        for (int v : report.values) out += "\n" + report.title + "," + std::to_string(v);
        return out;
    }
};

class JsonExporter : public Exporter {
private:
    std::string format_name() const override { return "json"; }
    std::string write_body(const Report& report) const override {
        std::string out = "{\"title\":\"" + report.title + "\",\"values\":[";
        for (std::size_t i = 0; i < report.values.size(); ++i) out += (i > 0 ? "," : "") + std::to_string(report.values[i]);
        return out + "]}";
    }
};

int main() {
    const CsvExporter csv;
    const JsonExporter json;
    const std::array<const Exporter*, 2> exporters{&csv, &json};

    const Report sales{"sales", {120, 95}};
    const Report returns{"returns", {}};
    for (const Exporter* e : exporters) {
        e->export_report(sales);
        e->export_report(returns);
    }
#ifdef CALL_HOOK
    csv.write_body(sales);  // outside code cannot skip the checks
#endif
}
Two formats, one set of checks Python · exporter_session.py
from session import run

# exporter.cpp built and run with both compilers, then with -DCALL_HOOK, which calls a hook directly.
run("""
clang++ -std=c++23 -Wall -Wextra exporter.cpp -o exporter && ./exporter
g++-16 -std=c++23 -Wall -Wextra exporter.cpp -o exporter-gcc && ./exporter-gcc
clang++ -std=c++23 -Wall -Wextra -DCALL_HOOK -c exporter.cpp
g++-16 -std=c++23 -Wall -Wextra -DCALL_HOOK -c exporter.cpp
""", max_lines=14)

Output

$ clang++ -std=c++23 -Wall -Wextra exporter.cpp -o exporter && ./exporter
[csv] exporting "sales" (2 values)
title,value
sales,120
sales,95
[csv] done, 30 bytes
[csv] refused "returns": no values
[json] exporting "sales" (2 values)
{"title":"sales","values":[120,95]}
[json] done, 35 bytes
[json] refused "returns": no values
$ g++-16 -std=c++23 -Wall -Wextra exporter.cpp -o exporter-gcc && ./exporter-gcc
[csv] exporting "sales" (2 values)
title,value
sales,120
sales,95
[csv] done, 30 bytes
[csv] refused "returns": no values
[json] exporting "sales" (2 values)
{"title":"sales","values":[120,95]}
[json] done, 35 bytes
[json] refused "returns": no values
$ clang++ -std=c++23 -Wall -Wextra -DCALL_HOOK -c exporter.cpp
exporter.cpp:68:9: error: 'write_body' is a private member of 'CsvExporter'
   68 |     csv.write_body(sales);  // outside code cannot skip the checks
      |         ^
exporter.cpp:39:17: note: declared private here
   39 |     std::string write_body(const Report& report) const override {
      |                 ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -DCALL_HOOK -c exporter.cpp
exporter.cpp: In function 'int main()':
exporter.cpp:68:19: error: 'virtual std::string CsvExporter::write_body(const Report&) const' is private within this context
   68 |     csv.write_body(sales);  // outside code cannot skip the checks
      |     ~~~~~~~~~~~~~~^~~~~~~
exporter.cpp:39:17: note: declared private here
   39 |     std::string write_body(const Report& report) const override {
      |                 ^~~~~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exporter_session.py

Step through one call of export_report() for each report, then the build that tries to skip it:

Interactive · stage by stage export_report(), the one way in Step through the CSV exporter with the sales report, then with the empty returns report. Each stage is a step of export_report(); only the hook belongs to CsvExporter.
export_report(), the one way in, step 8 of 8. Showing export_report(report) (json, both reports), Any values?, Log: exporting, write_body(report) (JsonExporter's: 35 bytes), Log: done, N bytes, Log: refused. In focus: write_body(report).no valuesexport_report(report)json, both reportsAny values?the shared checkLog: exportingthe shared first log linewrite_body(report)JsonExporter's: 35 bytesLog: done, N bytesthe shared last log lineLog: refusedreturns falseprivate

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The only way in. main calls export_report(sales) on the CSV exporter. It is the only public function, and it is not virtual, so its steps are the same for every format.
  2. The shared check. The sales report holds 2 values, so the check lets it through. CsvExporter never wrote this check.
  3. The shared log line. Exporter prints [csv] exporting "sales" (2 values). The format name comes from a hook too, so the line is right for every format.
  4. The hook. Only now does the part that differs run: CsvExporter's write_body() builds the CSV text, title,value and two rows, 30 bytes in all.
  5. The shared end. Exporter prints the body, then [csv] done, 30 bytes. CsvExporter contains no log line at all.
  6. An empty report. export_report(returns) meets an empty list of values, so the check sends it aside: [csv] refused "returns": no values. The hook never runs.
  7. The second format. JsonExporter overrides the same two hooks and nothing else, yet it runs the same check and the same log lines: its body is 35 bytes, and it refuses returns too.
  8. No way around it. Built with -DCALL_HOOK, main calls csv.write_body(sales) directly, and both compilers refuse: the hook is a private member of CsvExporter.

CsvExporter and JsonExporter contain no checks and no log lines, yet both refuse the empty “returns” report and log every export in the same words, because only Exporter::export_report() is public. The derived classes override private functions, which surprises many people: access controls who may call a function, not who may override it. The standard checks access at the call, with the static type, so export_report() may call its own private hooks and outside code may not, as the last two builds show.

The C++ FAQ of the Standard C++ Foundation describes this design under private virtuals: public non-virtual functions define the interface, and private or protected virtual functions define what derived classes may customise. Use it when every implementation must follow the same steps, such as checks, logging, locking or timing.

Checkpoint

Check: The non-virtual interface idiom

  1. Question 1 of 2 For the sales report, CsvExporter's hook builds title,value, then sales,120 and sales,95 on lines of their own. How many bytes did the shared last log line report?

    Type a number.

    A hint

    Count every character of the three lines, and the two line breaks between them.

    Show the answer: Question 1 of 2

    Answer: 30 bytes

    title,value is 11 characters, sales,120 is 9 and sales,95 is 8, with a line break before each row: 30 bytes, as [csv] done, 30 bytes said. Exporter printed that line, not CsvExporter.

  2. Question 2 of 2 Built with -DCALL_HOOK, main calls csv.write_body(sales); directly. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Both compilers reject it, because write_body is a private member of CsvExporter

    Private virtual hooks may be overridden by derived classes and called by the base's own functions, but not by outside code, so there is no way around the checks.

A pure virtual call during construction is undefined behaviour

A pure virtual function has no body to run. Normally no call can reach it, because every object you can create has overridden it, but there is one moment when the object is still only the abstract class: during the base class’s constructor and destructor.

pure_call.cpp C++ · pure_call.cpp
// Job's constructor calls start(), which calls the pure virtual function title(). While Job is being constructed,
// the object is only a Job, and Job has no title().
#include <print>

class Job {
public:
    Job() { start(); }
    virtual ~Job() = default;
    void start() const { std::println("starting {}", title()); }

private:
    virtual const char* title() const = 0;
};

class Backup : public Job {
private:
    const char* title() const override { return "backup"; }
};

int main() {
    Backup nightly;
    std::println("this line is never printed");
}
Both builds compile, and both programs stop Python · pure_call_session.py
from session import run

# pure_call.cpp: both builds compile without a warning; each program is then started on its own, so that the
# session names the signal that stops it.
run("""
clang++ -std=c++23 -Wall -Wextra pure_call.cpp -o pure_call
./pure_call
g++-16 -std=c++23 -Wall -Wextra pure_call.cpp -o pure_call-gcc
./pure_call-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra pure_call.cpp -o pure_call
$ ./pure_call
libc++abi: Pure virtual function called!
[stopped by SIGABRT]
$ g++-16 -std=c++23 -Wall -Wextra pure_call.cpp -o pure_call-gcc
$ ./pure_call-gcc
pure virtual method called
terminate called without an active exception
[stopped by SIGABRT]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pure_call_session.py

Step through the creation of nightly up to the moment the program stops:

Interactive · stage by stage Backup nightly; meets a function with no body Step through the creation of a Backup. Each stage is one step the program takes before it stops.
Backup nightly; meets a function with no body, step 6 of 6. Showing Backup nightly;, Job() runs (the object is only a Job), start() calls title(), title() in Job, SIGABRT. In focus: start() calls title().Backup nightly;main creates a BackupJob() runsthe object is only a Jobstart() calls title()ordinary, then virtualtitle() in Jobpure virtual: no bodySIGABRTthe program stopscall it once the object exists

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The base part first. Backup nightly; must build its Job part before anything of Backup exists.
  2. Only a Job, for now. While Job's constructor runs, the object is a Job, as in the previous lesson. For this moment it is an object of an abstract class.
  3. A call hidden in a helper. Job() calls start(), an ordinary function, and start() calls title(), a virtual one. Neither compiler warned, because the call is hidden inside start().
  4. Nothing to run. The virtual call runs the version of the class being constructed, Job, and Job's title() is pure virtual: there is no body to run. The standard calls this undefined behaviour.
  5. Both programs stop. Here both libraries stopped the program with SIGABRT: libc++abi printed Pure virtual function called! and libstdc++ pure virtual method called. The next line of main never ran.
  6. The fix. Never call virtual functions, directly or through a helper, from a constructor or a destructor. Create the Backup first, then call nightly.start() from outside.

Neither compiler warns, because the call is hidden inside start(), and the standard calls the result undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”. Here both runtime libraries stopped the program with SIGABRT, after libc++abi printed “Pure virtual function called!” and libstdc++ “pure virtual method called”. The fix is the one from the previous lesson: never call virtual functions, directly or through a helper, from constructors and destructors.

Starting work from the base constructor

Job() { start(); } runs start() while the object is only a Job, so its call to the pure title() has nothing to run. Let the constructor only build the object, and call nightly.start() once the Backup exists.

Checkpoint

Check: A pure virtual call while building

  1. Question 1 of 2 How did both builds of pure_call.cpp end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each runtime library printed a message about a pure virtual call, and the program stopped with SIGABRT

    The call to the pure title() during Job's constructor is undefined behaviour. With both standard libraries here, it ended the program with SIGABRT after a message.

  2. Question 2 of 2 Which change to pure_call.cpp removes the crash and still starts the backup?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Remove start() from Job's constructor and call nightly.start() in main after the Backup is created

    Rule C.82 of the C++ Core Guidelines: no virtual calls from constructors and destructors, directly or through a helper. Build the object first, then call the function that needs the derived class.

Practise: a logger interface and a fake

Thermostat logs a warning for each reading above its limit and an error from the third such reading in a row. It writes through a Logger&, but Logger is not an interface yet, so the tests cannot see what it logs.

The starter code and its sample tests

C header · logger.h

// Exercise: make every check in logger_tests.cpp pass with both compilers. Thermostat is finished: do not change it.
#pragma once
#include <format>
#include <print>
#include <string>
#include <vector>

enum class Level { info, warning, error };

inline const char* level_name(Level level) {
    switch (level) {
        case Level::info: return "info";
        case Level::warning: return "warning";
        case Level::error: return "error";
    }
    return "?";
}

// TODO 1: make Logger an interface. write() becomes a pure virtual function, and the class gets a virtual
// destructor; the printing it does now moves to ConsoleLogger.
class Logger {
public:
    void write(Level level, const std::string& message) { std::println("{}: {}", level_name(level), message); }
};

// TODO 2: ConsoleLogger prints each entry as "<level>: <message>".
class ConsoleLogger : public Logger {};

// TODO 3: FakeLogger keeps each entry in entries() instead of printing it, so that tests can check it.
class FakeLogger : public Logger {
public:
    struct Entry {
        Level level;
        std::string message;
    };
    const std::vector<Entry>& entries() const { return entries_; }

private:
    std::vector<Entry> entries_;
};

// The code under test. It logs a warning for each reading above the limit, and an error from the third such
// reading in a row.
class Thermostat {
public:
    Thermostat(Logger& log, double limit) : log_(log), limit_(limit) {}

    void reading(double celsius) {
        if (celsius <= limit_) {
            over_ = 0;
            return;
        }
        ++over_;
        log_.write(over_ >= 3 ? Level::error : Level::warning,
                   std::format("{:.1f} °C is over the limit of {:.1f} °C", celsius, limit_));
    }

private:
    Logger& log_;
    double limit_;
    int over_ = 0;
};

C++ · logger_tests.cpp

// Sample tests for logger.h. Each check prints what it expects when it fails; the exit status is 0 only when every
// check passes.
#include <print>
#include <type_traits>
#include <vector>

#include "logger.h"

namespace {
int checks = 0;
int passed = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (ok) ++passed;
    else std::println("FAILED: {}", what);
}
}  // namespace

int main() {
    check(std::is_abstract_v<Logger>, "Logger is an interface: no object of it can be created");
    check(std::has_virtual_destructor_v<Logger>, "Logger has a virtual destructor");

    FakeLogger fake;
    Thermostat thermostat{fake, 30.0};
    for (double celsius : {29.5, 31.0, 30.0, 32.5, 33.0, 34.0}) thermostat.reading(celsius);

    const auto& entries = fake.entries();
    check(entries.size() == 4, "four readings were over the limit, so the fake holds four entries");
    std::vector<Level> levels;
    for (const auto& e : entries) levels.push_back(e.level);
    check(levels == std::vector{Level::warning, Level::warning, Level::warning, Level::error},
          "the third reading over the limit in a row is an error");
    check(!entries.empty() && entries[0].message == "31.0 °C is over the limit of 30.0 °C",
          "a message names the reading and the limit");

    ConsoleLogger console;
    Logger& any = console;
    any.write(Level::info, "the console logger prints through the interface");

    std::println("{} of {} checks passed", passed, checks);
    return passed == checks ? 0 : 1;
}
The sample tests against the starter code Python · logger_session.py
from session import run

# The sample tests built against the starter code with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra logger_tests.cpp -o logger_tests && ./logger_tests
g++-16 -std=c++23 -Wall -Wextra logger_tests.cpp -o logger_tests-gcc && ./logger_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra logger_tests.cpp -o logger_tests && ./logger_tests
FAILED: Logger is an interface: no object of it can be created
FAILED: Logger has a virtual destructor
warning: 31.0 °C is over the limit of 30.0 °C
warning: 32.5 °C is over the limit of 30.0 °C
warning: 33.0 °C is over the limit of 30.0 °C
error: 34.0 °C is over the limit of 30.0 °C
FAILED: four readings were over the limit, so the fake holds four entries
FAILED: the third reading over the limit in a row is an error
FAILED: a message names the reading and the limit
info: the console logger prints through the interface
0 of 5 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra logger_tests.cpp -o logger_tests-gcc && ./logger_tests-gcc
FAILED: Logger is an interface: no object of it can be created
FAILED: Logger has a virtual destructor
warning: 31.0 °C is over the limit of 30.0 °C
warning: 32.5 °C is over the limit of 30.0 °C
warning: 33.0 °C is over the limit of 30.0 °C
error: 34.0 °C is over the limit of 30.0 °C
FAILED: four readings were over the limit, so the fake holds four entries
FAILED: the third reading over the limit in a row is an error
FAILED: a message names the reading and the limit
info: the console logger prints through the interface
0 of 5 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 logger_session.py

The warning: and error: lines in the output are the clue: Thermostat prints through Logger::write(), so FakeLogger never sees a thing.

  1. TODO 1: make Logger an interface, with write() pure virtual and a virtual destructor.
  2. TODO 2: give ConsoleLogger the printing that Logger::write() does now.
  3. TODO 3: make FakeLogger store every call in entries() instead of printing it.

When both compilers print 5 of 5 checks passed, the only line before it should be the console logger’s own message. Then add a test of your own: a reading exactly at the limit must not be logged.

Then take the five ideas of this lesson, from the class that stays abstract to the call with no body, to cases it has not run. Whatever your answers show as weakest comes first, and every choice says what it gets right or misses:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What makes a C++ class abstract?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: It has at least one pure virtual function that no class on the way has overridden

    A pure virtual function, written with = 0, makes its class abstract, and a derived class stays abstract until every pure virtual function it inherits has an override. C++ has no abstract or interface keyword.

  2. Question 2 of 15 PaymentMethod declares charge() and name() as pure virtual. What happens with this class?

    Read the code, then choose one answer.

    class GiftCard : public PaymentMethod {
    public:
        std::string charge(Money, const std::string&) override { return "gift"; }
    };
    GiftCard card;
    Show the answer: Question 2 of 15

    Answer: GiftCard card; does not compile, because name() is still pure virtual

    GiftCard inherits the pure virtual name() and does not override it, so GiftCard is abstract too. Both compilers reject the variable and list the function that is still missing.

  3. Question 3 of 15 A function is declared PaymentMethod best_method();, returning the abstract class by value. Which statement is true?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: The declaration compiles, but a definition of the function does not

    No object of an abstract class can exist, and a by-value result is one. Return a reference, a pointer or a std::unique_ptr<PaymentMethod> instead.

  4. Question 4 of 15 PaymentMethod is an interface. Which of these does it not have?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: Data members

    An interface in C++ is a pure abstract class: pure virtual functions and a public virtual destructor, no data and no constructors.

  5. Question 5 of 15 A new UpiPayment class implements PaymentMethod. What must change in Checkout?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: Nothing; a Checkout can be given the new class like any other PaymentMethod

    Code that depends only on an interface is closed to change when a new implementation arrives: the new class overrides the pure functions, and existing callers keep working.

  6. Question 6 of 15 A program keeps its payment methods in std::vector<std::unique_ptr<PaymentMethod>>, holding a CardPayment and a WalletPayment. What must PaymentMethod have so that the vector deletes them correctly?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: A public virtual destructor

    An interface that implementations are deleted through needs a public virtual destructor, as PaymentMethod's virtual ~PaymentMethod() = default; provides. That is the rule of the previous lesson, applied to interfaces.

  7. Question 7 of 15 Why can the tests of Checkout run without any card network or wallet?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: Checkout depends only on the PaymentMethod interface, so a test can hand it a fake implementation

    A class that takes a reference to an interface works with every implementation, including one written only for tests. The fake can approve or decline on command and record what it was asked to do.

  8. Question 8 of 15 Which of the five checks could a test write only because the fake records every amount it is charged?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: the method is charged once, with the exact amount

    A fake can check how the code used the interface: how often it called, with which arguments. Two of the five checks read the record, the second and the last.

  9. Question 9 of 15 A new rule says Checkout must never charge an order above 50,000.00. Which test, with the fake, proves the rule?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: Pay 5'000'001, which is 50,000.01, and check that the fake recorded no charge

    Write the test from what the rule forbids: an order over the limit must not reach the payment method at all, and the fake's record of calls is where that shows.

  10. Question 10 of 15 In the non-virtual interface idiom, where does a check that every implementation needs belong?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: In the base class's public non-virtual function, which runs the check and then calls the virtual hook

    The public function is written once, in the base class, so the check and the log lines run for every implementation, including ones written later. Derived classes only override the private hooks.

  11. Question 11 of 15 Can a derived class override a virtual function that its base class declares private?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Yes. Access decides who may call the function, not who may override it

    CsvExporter overrides the private hooks of Exporter, and Exporter's own export_report() calls them. Outside code cannot call the hooks, which is what makes the idiom work: there is no way around the checks.

  12. Question 12 of 15 A new XmlExporter overrides format_name() and write_body() and nothing else. What happens when it is asked to export the empty returns report?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: It refuses it, logging [xml] refused "returns": no values, like the other formats

    With the non-virtual interface idiom every new implementation follows the same steps automatically: the check, the log lines and then its own hooks.

  13. Question 13 of 15 What does the standard say about a virtual call that reaches a pure virtual function while the abstract class's constructor runs?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: The behaviour is undefined

    A virtual call from a constructor or destructor runs the version of the class being built or destroyed, and when that version is pure virtual, the behaviour is undefined.

  14. Question 14 of 15 Job's constructor calls start(), which calls the pure virtual title(). What happened when Backup, derived from Job, was created?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: Both programs stopped with SIGABRT after the library printed a message about a pure virtual call

    During Job's constructor the object is a Job, and Job has no title(). A virtual call to a pure virtual function from a constructor is undefined behaviour; both libraries' runtimes stopped the program. No warning appeared, because the call was hidden inside start().

  15. Question 15 of 15 Job's constructor no longer calls start(), but its destructor now does. What happens when a Backup is destroyed?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: The same undefined behaviour, because the object is only a Job again while ~Job runs

    Destruction undoes construction, so while the base destructor runs the object is the base class again. A pure virtual function reached from there is undefined behaviour, as in the constructor.

Interview questions

Fresher · What is the difference between an abstract class and an interface in C++?

An abstract class is any class with at least one pure virtual function, declared with = 0, so no object of it can be created; it may also have data members, constructors and ordinary member functions with code. An interface is a convention for the strictest kind of abstract class: only pure virtual functions and a public virtual destructor, and no data. C++ has no interface keyword, so both are written as classes. Use an abstract class when implementations share code or data; use an interface when you only want to promise what a type can do, which keeps implementations independent and makes fakes for tests easy to write. A good answer mentions that a class can implement several interfaces through multiple inheritance.

The follow-ups usually test the edges of that answer: one class with two interfaces, and an interface whose destructor is pure. Decide both:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 A Thermostat must be usable as a Sensor and as a Display, two interfaces with one pure virtual function each. Can one class implement both?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Yes: class Thermostat : public Sensor, public Display, overriding the pure functions of both

    A class can implement several interfaces through multiple inheritance, and because interfaces have no data members, they add only functions to override. That is what the warm-up answer meant.

  2. Question 2 of 2 An interface declares virtual ~PaymentMethod() = 0;. What else must the program contain?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A definition outside the class, such as PaymentMethod::~PaymentMethod() = default;

    A pure virtual destructor makes the class abstract, but it is still called, from the destructor of every derived class, so it must be defined outside the class.

Two harder questions of this module start from this lesson: a service whose tests send real payment requests, and a crash that reports a pure virtual function call:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    OrderService calls a payment gateway's client class directly, so its tests send real payment requests. How would you change the design so that OrderService can be tested without the network, and what does each option cost?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A service occasionally crashes with "pure virtual function called". What can cause this message, and how would you find the call that produced it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A pure virtual function, virtual … = 0;, makes its class abstract: no object of it can be created, and a derived class stays abstract until it overrides every pure virtual function it inherits.
  • An interface in C++ is a class with only pure virtual functions and a public virtual destructor, and no data, used through references and pointers.
  • Code that depends on an interface works with every implementation, including a fake that a test controls and inspects.
  • The non-virtual interface idiom keeps shared steps in one public non-virtual function, which calls private virtual hooks; derived classes may override private virtual functions.
  • A virtual call to a pure virtual function from a constructor or destructor is undefined behaviour; with both standard libraries here, it stopped the program.

Terms in this lesson

abstract class
A class with at least one pure virtual function that it has not overridden. No object of it can be created, not even as a by-value parameter, but references and pointers to it can refer to objects of its concrete derived classes.
fake
A small implementation of an interface written for tests, such as a payment method that approves or declines on command and records every charge, so that a test can run the code without the real service and check how it was used.
interface
In C++, a class with only pure virtual functions and a public virtual destructor, and no data: it says what a type can do and leaves how to the classes that implement it. C++ has no interface keyword; this is a convention.
non-virtual interface idiom (NVI)
A design in which a base class's public function is not virtual: it runs the steps every implementation shares, such as checks and logging, and calls private virtual hooks that derived classes override for the part that differs.
pure virtual function
A virtual function declared with = 0 instead of a body, as in virtual std::string name() const = 0;. It makes its class abstract, and each derived class must override it before objects of that class can be created.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.