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
  1. 1What a virtual function call does
  2. 2override: let the compiler check the signature
  3. 3Pure virtual functions and abstract classes
  4. 4Why the base class destructor must be virtual
  5. 5Virtual calls in constructors, and “pure virtual function call”
  6. 6Default arguments, hidden overloads and slicing
  7. 7What a virtual function costs
  8. 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 line passes through an empty square frame and forks: a teal branch ends in an arrow at a hatched two-cell bar, a dashed grey one at a grey three-cell bar. Each bar starts with the frame’s square.
Figure 1 — In a virtual call, the object’s type, not the pointer’s, decides which function runs.

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 constexpr and be called in a constant expression (P1064R0). Under C++17, MSVC and both Clangs rejected the declaration; GCC 16.2 accepted it at -Wall -Wextra without a word (-pedantic warns that a member can be both virtual and constexpr only in C++20) and failed only when a static_assert evaluated 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 nothing override.

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&:

[Example 1:
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:

[Example 2:
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::content_type 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:

[Example 3:
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:

[Example 4:
CsvExporter::write: Q3 sales
Exporter::write: Q3 sales

— end example]

Write override on it, void write(std::ostream& out, const Report& r) override, and the mistake is a compile error on all four. GCC 16.2, then both Clangs:

[Example 5:
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.

[Example 6:
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):

[Example 7:
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:

[Example 8:
-- 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:

[Example 9:
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:

[Example 10:
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::~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:

[Example 11:
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:

[Example 12:
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:

[Example 13:
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::unique_ptr<Exporter> 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::unique_ptr<Exporter> needs the virtual destructor: its deleter calls delete on an Exporter*.
  • A std::shared_ptr<Exporter> made by std::make_shared<FileExporter> destroys the FileExporter it created: the standard destroys it through pu->~U(), where U is the type make_shared made ([util.smartptr.shared.create]), and all four ran ~FileExporter, close q3.csv and ~Exporter without a virtual destructor. Both Clangs still warned (-Wdelete-non-abstract-non-virtual-dtor) until FileExporter was final.

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):

[Example 14:
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]).

[Example 15:
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:

[Example 16:
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:

[Example 17:
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):

[Example 18:
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:

[Example 19:
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):

[Example 20:
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::write; brings it back:

[Example 21:
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::write'?.

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:

[Example 22:
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:

Two objects, CsvExporter and JsonExporter, each a vptr cell above its members. Each vptr points to its class’s vtable, whose two slots point to that class’s write and content_type functions.
Figure 2 — In the Itanium and Microsoft ABIs, each object of a class with virtual functions carries a pointer to its class's table, and a virtual call reads the function's address from that table.

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:

[Example 23:
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:

  • override on every overriding function, final where no override may follow, and virtual only 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-override and -Wnon-virtual-dtor on GCC and Clang, /w14263 /w14264 /w14265 /w15204 on MSVC, and clang-tidy’s bugprone-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::has_virtual_destructor 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.

Read next

  1. Rule of zero: the special members C++ writes for youLanguage · Miriam Castellanos · written for C++20
  2. C++ move semantics: what std::move and && really doLanguage · Miriam Castellanos · written for C++20
  3. Can a C++ destructor throw? What happens since C++11Language · Miriam Castellanos · written for C++20