Language[lang]
C++ virtual functions: override, pure virtual and the traps
- Section
- Language [lang]
- Standard
- C++11
- Compilers
- MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
- Published
- Length
- 9 min read
In this answer8 clauses
- 1What a virtual function call does
- 2override: let the compiler check the signature
- 3Pure virtual functions and abstract classes
- 4Why the base class destructor must be virtual
- 5Virtual calls in constructors, and “pure virtual function call”
- 6Default arguments, hidden overloads and slicing
- 7What a virtual function costs
- 8Polymorphism in C++: virtual functions, std::variant or a template
Abstract
A C++ virtual function is a member function whose call through a base pointer or reference is decided by the type of the object, not the type of the pointer. Declare it virtual in the base, write override (C++11) on every function that overrides it so a signature mismatch is a compile error, and give a base deleted through a base pointer a virtual destructor. A virtual call in a base constructor or destructor runs the base version; if that version is pure, the behavior is undefined.

A C++ virtual function is a member function whose call is decided by the object’s type, not the type of the pointer or reference: through an Exporter&, write runs JsonExporter::write when the object is a JsonExporter. Declare it virtual in the base and write override on each function that overrides it, so a signature that does not match is a compile error. Give a base deleted through a base pointer a virtual destructor. In a base’s constructor or destructor, a virtual call runs the base’s version; if that version is pure, the behavior is undefined.
This answer is written for C++11, which added override and final; virtual itself is older. The rules are in [class.virtual], and the first GCC, Clang and MSVC releases with override are in Since When’s C++11 table. Since then:
- C++20 lets a virtual function be
constexprand be called in a constant expression (P1064R0). Under C++17, MSVC and both Clangs rejected the declaration; GCC 16.2 accepted it at-Wall -Wextrawithout a word (-pedanticwarns that a member can be bothvirtualandconstexpronly in C++20) and failed only when astatic_assertevaluated the call. - C++23: a member function with an explicit object parameter cannot be virtual. All four compilers below rejected
virtual std::.string content_type(this const Exporter& self);
The samples were built with four compilers on three standard libraries: MSVC 19.44 (/W4 /permissive-) and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64), the other three with -Wall -Wextra, unoptimized. MSVC has no C++11 mode and Microsoft’s STL needs C++14, so all four built C++14 unless the text names another standard; GCC 16.2 built 21 of them again under -std=c++11, with the same results. An output shown is what all four printed.
What changed: an overriding function is marked virtual again, or nothingoverride.
since C++11
What a virtual function call does
A Report is a title and a few rows. write is virtual and content_type is not; save calls both through a const Exporter&:
struct Report {
std::string title;
std::vector<std::string> rows;
};
class Exporter {
public:
virtual ~Exporter() = default;
virtual void write(std::ostream& out, const Report& r) const {
out << "Exporter::write: " << r.title << '\n';
}
std::string content_type() const { return "text/plain"; }
};
class JsonExporter : public Exporter {
public:
void write(std::ostream& out, const Report& r) const override {
out << "JsonExporter::write: " << r.title << '\n';
}
std::string content_type() const { return "application/json"; }
};
void save(const Exporter& exporter, const Report& r) {
exporter.write(std::cout, r); // virtual: the object decides
// not virtual: the reference's type decides
std::cout << exporter.content_type() << '\n';
}
// ...
const Report report{"Q3 sales", {"north,120", "south,95"}};
JsonExporter json;
save(json, report); // JsonExporter::write, then text/plain
const Exporter* exporter = &json;
exporter->write(std::cout, report); // JsonExporter::write
exporter->Exporter::write(std::cout, report); // qualified: base— end example]
It printed:
JsonExporter::write: Q3 sales
text/plain
JsonExporter::write: Q3 sales
Exporter::write: Q3 sales— end example]
The virtual call runs the final overrider, the version in the most derived class that overrides it. JsonExporter:: hides the base’s without overriding it, and no compiler warned.
override: let the compiler check the signature
A function overrides a virtual one only if its name, parameter types, cv-qualifiers and ref-qualifiers all match. The same parameters with another return type are an error, not a second function, unless the return type is covariant: a pointer or reference to a class derived from the one the base returns, as in a clone() ([class.virtual]). Leave const off and you have declared a second function:
class CsvExporter : public Exporter {
public:
// const left off: a second function, not an override
void write(std::ostream& out, const Report& r) {
out << "CsvExporter::write: " << r.title << '\n';
}
};
// ...
CsvExporter csv;
const Exporter& exporter = csv;
csv.write(std::cout, report); // CsvExporter::write
exporter.write(std::cout, report); // Exporter::write— end example]
It compiled on all four and printed:
CsvExporter::write: Q3 sales
Exporter::write: Q3 sales— end example]
Write override on it, void write(std::, and the mistake is a compile error on all four. GCC 16.2, then both Clangs:
error: 'void CsvExporter::write(std::ostream&, const Report&)' marked 'override', but does not override
error: non-virtual member function marked 'override' hides virtual member function— end example]
MSVC’s is C3668, method with override specifier 'override' did not override any base class methods. final closes the chain: a final function cannot be overridden, and a final class cannot be derived from.
class JsonExporter : public Exporter {
public:
void write(std::ostream& out, const Report& r) const final {
// ...
class PrettyJsonExporter : public JsonExporter {
public:
// error: JsonExporter::write is final
void write(std::ostream& out, const Report& r) const override {
// ...
class CsvExporter final : public Exporter {
// ...
// error: CsvExporter is final
class TsvExporter : public CsvExporter {};— end example]
All four rejected both (MSVC with C3248 and C3246). Core Guidelines C.128 asks for exactly one of virtual, override and final on each declaration.
Without override, what warns depends on the mistake. At the flags above, and with the flag that adds a warning:
| Mistake | MSVC 19.44 | GCC 16.2 | Clang 19.1 and 23.1 | clang-tidy 19.1.5 |
|---|---|---|---|---|
const left off |
C4263 and C4264, only with /w14263 /w14264 |
-Woverloaded-virtual=1, in -Wall |
-Woverloaded-virtual, on at -Wall |
no report |
| Correct override, not marked | no warning, with or without those flags | only with -Wsuggest-override |
-Winconsistent-missing-override if the class marks another; all with -Wsuggest-override |
modernize-use-override |
content_types for content_type |
no warning | no warning | no warning | bugprone-virtual-near-miss |
With override, the misspelled name is an error too: a function marked override that overrides nothing is ill-formed. MSVC’s /analyze with the Core Check rules reported the unmarked overrides as C26433; clang-tidy is among the analyzers on the Toolchain page.
Pure virtual functions and abstract classes
= 0 makes a virtual function pure, and a class with a pure virtual function abstract: it can only be a base.
| Kind | Overriding it | Makes the class abstract | Its body |
|---|---|---|---|
| Virtual function | optional: the base’s version runs otherwise | no | the default behavior |
| Pure virtual function | required before an object can exist | yes | optional, run by a qualified call; required for a destructor |
An interface is a pure abstract class, with no data (Core Guidelines C.121):
class Exporter {
public:
virtual ~Exporter() = default;
virtual void write(std::ostream& out, const Report& r) const = 0;
virtual std::string content_type() const = 0;
};
class CsvExporter : public Exporter {
public:
void write(std::ostream& out, const Report& r) const override {
out << r.title << '\n';
for (const auto& row : r.rows) out << row << '\n';
}
std::string content_type() const override { return "text/csv"; }
};
// ...
std::vector<std::unique_ptr<Exporter>> exporters;
exporters.push_back(std::make_unique<CsvExporter>());
exporters.push_back(std::make_unique<JsonExporter>());
for (const auto& exporter : exporters) {
std::cout << "-- " << exporter->content_type() << '\n';
exporter->write(std::cout, report);
}— end example]
With a JsonExporter beside it (std::make_unique is C++14), it printed:
-- text/csv
Q3 sales
north,120
south,95
-- application/json
{"title": "Q3 sales", "rows": 2}— end example]
Exporter exporter; compiled on none of the four. Both Clangs named what is missing:
error: variable type 'Exporter' is an abstract class
note: unimplemented pure virtual method 'write' in 'Exporter'
note: unimplemented pure virtual method 'content_type' in 'Exporter'— end example]
A derived class stays abstract until it overrides every pure function: a CsvExporter without content_type failed the same way (MSVC: C2259, cannot instantiate abstract class).
A pure virtual function may still have a body, defined outside the class: one declaration cannot have both = 0 and a body ([class.abstract]). A derived class still has to override it before objects of that class can exist, and its override can call the body with a qualified name:
class Exporter {
public:
virtual ~Exporter() = default;
// pure: every exporter must write; the body is a shared default
virtual void write(std::ostream& out, const Report& r) const = 0;
};
void Exporter::write(std::ostream& out, const Report& r) const {
out << "# " << r.title << '\n';
}
class CsvExporter : public Exporter {
public:
void write(std::ostream& out, const Report& r) const override {
Exporter::write(out, r); // a qualified call runs the body
for (const auto& row : r.rows) out << row << '\n';
}
};— end example]
A pure virtual destructor needs a body too, because every derived destructor calls it: without one, virtual ~Exporter() = 0; failed on all four at link time, not at compile time (MSVC LNK2019, GCC undefined reference to Exporter::).
Why the base class destructor must be virtual
delete through a pointer to the base is undefined behavior when the object is a derived class and the base’s destructor is not virtual, unless the base declares a C++20 destroying operator delete ([expr.delete]). With a virtual destructor, every destructor runs, the derived class’s first. FileExporter owns a handle whose destructor prints:
struct FileHandle { // stands in for an open file
std::string path;
~FileHandle() { std::cout << "close " << path << '\n'; }
};
class Exporter {
public:
virtual ~Exporter() { std::cout << "~Exporter\n"; }
virtual void write(std::ostream& out, const Report& r) const = 0;
};
class FileExporter : public Exporter {
public:
explicit FileExporter(std::string path)
: file_{std::move(path)} {}
~FileExporter() override { std::cout << "~FileExporter\n"; }
void write(std::ostream& out, const Report& r) const override {
out << "write " << r.title << " to " << file_.path << '\n';
}
private:
FileHandle file_;
};
// ...
Exporter* exporter = new FileExporter("q3.csv");
exporter->write(std::cout, report);
delete exporter; // ~FileExporter, close q3.csv, ~Exporter— end example]
It printed:
write Q3 sales to q3.csv
~FileExporter
close q3.csv
~Exporter— end example]
The same classes without virtual on ~Exporter were compiled and never run, because the delete is undefined behavior:
class Exporter {
public:
~Exporter() { std::cout << "~Exporter\n"; } // not virtual
virtual void write(std::ostream& out, const Report& r) const = 0;
};
// ...
Exporter* exporter = new FileExporter("q3.csv");
exporter->write(std::cout, report);
delete exporter; // undefined behavior— end example]
What warned at the flags above, and the flags that warn about the class:
| Base class | MSVC 19.44 | GCC 16.2 | Clang 19.1 and 23.1 |
|---|---|---|---|
| Abstract, destructor not virtual | C5205 | -Wdelete-non-virtual-dtor |
-Wdelete-abstract-non-virtual-dtor |
| Concrete, no destructor declared | no warning | -Wdelete-non-virtual-dtor |
-Wdelete-non-abstract-non-virtual-dtor |
Abstract, held in a std:: |
C5205 | no warning | a warning from inside the library |
| A warning on the class, off at these flags | /w14265 /w15204 |
-Wnon-virtual-dtor |
-Wnon-virtual-dtor |
clang-tidy’s cppcoreguidelines-virtual-class-destructor reported destructor of 'Exporter' is public and non-virtual, with a fix-it that inserts virtual. The rule it checks is Core Guidelines C.35: a base class destructor is public and virtual, or protected and non-virtual. Smart pointers:
- A
std::needs the virtual destructor: its deleter callsunique_ptr<Exporter> deleteon anExporter*. - A
std::made byshared_ptr<Exporter> std::destroys themake_shared<FileExporter> FileExporterit created: the standard destroys it throughpu->~U(), whereUis the typemake_sharedmade ([util.smartptr.shared.create]), and all four ran~FileExporter,close q3.csvand~Exporterwithout a virtual destructor. Both Clangs still warned (-Wdelete-non-abstract-non-virtual-dtor) untilFileExporterwasfinal.
Declaring a destructor, even virtual ~Exporter() = default;, suppresses the implicit move operations of that class (the rule of zero answer has the table), and only of that class: moving a CsvExporter copied the base’s member (1 copies, 0 moves) and still moved its own. To keep the moves, default the copy and move operations in the base, protected, since Core Guidelines C.67 asks a polymorphic class to suppress public copy and move (Counter is a member that counts its own copies and moves):
class Exporter {
public:
Exporter() = default;
virtual ~Exporter() = default;
virtual std::string content_type() const { return "text/plain"; }
Counter header;
protected: // derived classes copy and move; nobody slices
Exporter(const Exporter&) = default;
Exporter(Exporter&&) = default;
Exporter& operator=(const Exporter&) = default;
Exporter& operator=(Exporter&&) = default;
};— end example]
The base’s member then moved too: header: 0 copies, 1 moves.
Virtual calls in constructors, and “pure virtual function call”
While Exporter’s constructor or destructor runs, the object is an Exporter: a virtual call there runs Exporter’s version, not the derived class’s ([class.cdtor]).
class Exporter {
public:
Exporter() {
std::cout << "constructing: " << content_type() << '\n';
}
virtual ~Exporter() {
std::cout << "destroying: " << content_type() << '\n';
}
virtual std::string content_type() const { return "text/plain"; }
};
class JsonExporter : public Exporter {
public:
std::string content_type() const override {
return "application/json";
}
};
// ...
JsonExporter json;
std::cout << "constructed: " << json.content_type() << '\n';— end example]
It printed:
constructing: text/plain
constructed: application/json
destroying: text/plain— end example]
If that version is pure, the call is undefined behavior, made directly or through another member function, even when the pure function has a body ([class.abstract]). This constructor makes it through a helper, and no compiler warned:
class Exporter {
public:
Exporter() { announce(); } // calls a pure function, indirectly
virtual ~Exporter() = default;
virtual std::string content_type() const = 0;
private:
void announce() const {
const std::string type = content_type();
std::cout << "new exporter: " << type << '\n';
}
};
// ...
std::cout << "start" << std::endl;
JsonExporter json; // the program ends in here— end example]
It was run on purpose, at -O0 and -O2 alike. After start:
| Built with | What it printed | How it ended |
|---|---|---|
| MSVC 19.44, Clang 19.1 (Microsoft’s runtime) | nothing | exit code 0xC0000409 |
| GCC 16.2, libstdc++ | pure virtual method called, then terminate called without an active exception |
SIGABRT |
| Clang 23.1, libc++ | libc++abi: Pure virtual function called! |
SIGABRT |
Microsoft documents this error as R6025, “pure virtual function call”; these release builds showed no text. Their exit code, 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN), does not mean a buffer overran: the release runtime’s abort() ends the program through __fastfail, which Windows reports with that code. Called directly in the constructor (compiled, not run), the same call drew warnings: GCC 16.2 reported a pure virtual function called from constructor, and both Clangs -Wcall-to-pure-virtual-from-ctor-dtor. MSVC said nothing and failed to link (LNK2019).
The same crash can come at shutdown: a virtual call on an object whose destructor is running, from another thread or through a dangling pointer, can reach the base’s pure function, because during destruction the object is a base again ([class.cdtor]).
The fix is to make the call once the object is complete. A factory function constructs, then calls (Core Guidelines C.50):
class Exporter {
public:
virtual ~Exporter() = default;
virtual std::string content_type() const = 0;
template <class T>
static std::unique_ptr<T> create() {
auto exporter = std::make_unique<T>();
exporter->announce(); // the object is complete now
return exporter;
}
private:
void announce() const {
std::cout << "new exporter: " << content_type() << '\n';
}
};
// ...
auto json = Exporter::create<JsonExporter>();— end example]
It printed new exporter: application/json. When the base needs only a value, pass it to the base’s constructor:
class Exporter {
public:
explicit Exporter(std::string type) : type_(std::move(type)) {
std::cout << "constructing: " << type_ << '\n';
}
virtual ~Exporter() = default;
const std::string& content_type() const { return type_; }
private:
std::string type_;
};
class JsonExporter : public Exporter {
public:
JsonExporter() : Exporter("application/json") {}
};— end example]
It printed constructing: application/json.
Default arguments, hidden overloads and slicing
No compiler warned about any of these three at the flags above, and each program printed what its comments say.
A default argument comes from the static type of the call, the function from the dynamic type ([dcl.fct.default]; Core Guidelines C.140):
class Exporter {
public:
virtual ~Exporter() = default;
virtual void write(std::ostream& out, const Report& r,
int indent = 0) const {
// ...
class JsonExporter : public Exporter {
public:
void write(std::ostream& out, const Report& r,
int indent = 2) const override {
// ...
JsonExporter json;
const Exporter& exporter = json;
exporter.write(std::cout, report); // JsonExporter's, indent 0
json.write(std::cout, report); // JsonExporter's, indent 2— end example]
A function in the derived class hides every base function of the same name. Overriding the two-argument write hides the base’s one-argument overload (C.138), and using Exporter:: brings it back:
class Exporter {
public:
virtual ~Exporter() = default;
virtual void write(std::ostream& out, const Report& r) const {
out << r.title << '\n';
}
void write(const Report& r) const { write(std::cout, r); }
};
class CsvExporter : public Exporter {
public:
using Exporter::write; // brings back write(const Report&)
void write(std::ostream& out, const Report& r) const override {
// ...
CsvExporter csv;
csv.write(report); // Exporter::write(r), then CsvExporter's— end example]
Without the using line, csv.write(report) failed on all four; both Clangs asked did you mean 'Exporter::.
Copying a derived object into a base object copies only the base part, and the copy calls the base’s functions. Hold polymorphic objects by reference, pointer or smart pointer:
JsonExporter json;
Exporter copy = json; // copies the Exporter part
std::vector<Exporter> all{json}; // so does this
const Exporter& ref = json; // a reference is not a copy
std::cout << copy.content_type() << '\n'; // text/plain
std::cout << all.front().content_type() << '\n'; // text/plain
std::cout << ref.content_type() << '\n'; // application/json— end example]
A protected copy constructor in the base, as in the Exporter that keeps its moves above, makes Exporter copy = json; an error on all four; both Clangs: calling a protected constructor of class 'Exporter'.
What a virtual function costs
The standard says which function a virtual call runs, not how. The two ABIs these compilers follow, the Itanium C++ ABI (GCC and Clang on Linux) and Microsoft’s (MSVC and Clang 19.1 on Windows), put a pointer, the vptr, to a per-class table in every object of a class with virtual functions:

The figure draws two slots per table; in the Itanium ABI a real table also holds the offset to the top of the object, a pointer to the type information and, for a virtual destructor, two entries. What that pointer adds, on x64:
struct PlainExporter { // an int, no virtual function
int indent;
void write(std::ostream&, const Report&) const {}
};
struct Exporter { // the same with one virtual function
int indent;
virtual void write(std::ostream&, const Report&) const {}
};
struct FullExporter { // two virtual functions
int indent;
virtual void write(std::ostream&, const Report&) const {}
virtual std::string content_type() const { return "text/plain"; }
};
struct CsvExporter : Exporter { // a derived class adds an int
int columns;
};
struct Flushable { // a second polymorphic base
virtual void flush() {}
};
struct FileExporter : Exporter, Flushable {}; // two vptrs— end example]
| Type | Itanium ABI: GCC 16.2, Clang 23.1 | Microsoft ABI: MSVC 19.44, Clang 19.1 |
|---|---|---|
void* |
8 | 8 |
PlainExporter: an int |
4 | 4 |
Exporter: an int, one virtual function |
16 | 16 |
FullExporter: an int, two virtual functions |
16 | 16 |
CsvExporter: Exporter and an int |
16 | 24 |
Flushable: one virtual function, no data |
8 | 8 |
FileExporter: two polymorphic bases |
24 | 24 |
A second virtual function adds a slot to the table, not a byte to the object. The ABIs differ on CsvExporter alone: the Itanium ABI puts the derived class’s int in the base’s tail padding. When the compiler can prove which function a call reaches, it may call it directly: a call through a final class or to a final function is one such case, and GCC’s optimizer looks for others under -fdevirtualize, on at -O2, -O3 and -Os.
Polymorphism in C++: virtual functions, std::variant or a template
Virtual functions are run-time polymorphism; templates and overloading are compile-time polymorphism. Choose by who knows the set of types:
| The types | Use | Since |
|---|---|---|
| An open set that other code adds to | virtual functions, objects held by reference or std::unique_ptr |
before C++11 |
| A closed set, known in one place | std::variant and std::visit |
C++17 |
| Known at compile time at each call | a template, constrained by a concept | C++20 for concepts |
| One operation, any callable | std::function, or a template taking a lambda |
C++11 |
For each class hierarchy:
overrideon every overriding function,finalwhere no override may follow, andvirtualonly where a function is introduced (C.128).- A public virtual destructor in a base deleted through a base pointer; otherwise a protected non-virtual one (C.35).
- No virtual calls in constructors or destructors; a factory function instead (C.82, C.50).
- The same default arguments in an override as in the base (C.140).
using Base::name;where an override hides an overload callers still use (C.138).- Polymorphic objects held by reference, pointer or smart pointer, not copied into a base object.
- Warnings on:
-Wsuggest-overrideand-Wnon-virtual-dtoron GCC and Clang,/w14263 /w14264 /w14265 /w15204on MSVC, and clang-tidy’sbugprone-virtual-near-miss.
More answers about the core language are in Language.
Questions
Can a constructor be virtual in C++?
No. A constructor cannot be declared virtual: MSVC 19.44, GCC 16.2, Clang 19.1 and Clang 23.1 all rejected virtual Exporter() = default;, GCC with “constructors cannot be declared ‘virtual’”. To create an object whose type is chosen at run time, call a factory function that returns a std::unique_ptr to the base; to copy an object you hold only through a base pointer, give the base a virtual clone() that each derived class overrides (C++ Core Guidelines C.130).
Can a virtual function be private?
Yes. Access and overriding are independent: a derived class can override a virtual function of the base that it cannot call. The base calls it from a public non-virtual function, the non-virtual interface: a public save() writes a header, then calls the private virtual write(), and a CsvExporter that overrides write() as private printed its rows through save() on MSVC, GCC and Clang.
Can a function template be virtual?
No. A member function template cannot be virtual, and MSVC, GCC and Clang all rejected one; MSVC says "member function templates cannot be virtual" (C2898). A member function of a class template can be: an Exporter<Out> with a virtual write(Out&, const Report&) and a CsvExporter<Out> that overrides it compiled and ran on all four compilers: MSVC 19.44, GCC 16.2, Clang 19.1 and Clang 23.1.
Do I need virtual on the overriding function too?
No. A function that overrides a virtual function is virtual whether or not it says so. Write override alone: the C++ Core Guidelines (C.128) ask for exactly one of virtual, override and final on each declaration, and override turns a function that overrides nothing into a compile error.
Is a derived class destructor virtual automatically?
Yes, when the base class destructor is virtual: the derived destructor overrides it, whether the compiler declares it or you write it without virtual or override. std:: was true on MSVC, GCC and Clang for a derived class with no destructor and for one declaring ~FileExporter() {}. If the base destructor is not virtual, nothing in the derived class makes delete through a base pointer defined.
Sources
- virtual function specifiercppreference.com
- [class.virtual] Virtual functionsC++ standard working draft
- [class.cdtor] Construction and destructionC++ standard working draft
- [expr.delete] DeleteC++ standard working draft
- C.35: A base class destructor should be either public and virtual, or protected and non-virtualC++ Core Guidelines
- Itanium C++ ABIItanium C++ ABI
- C Runtime Error R6025Microsoft Learn