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 23 – Design, idioms and maintainable C++

Type erasure in C++: how std::function works

Build a type-erased C++ wrapper with the concept and model pattern, see how std::function and std::any hold any type, and measure a small buffer.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: Why not a base class, a template or a variant? · A concept and a model carry every call · std::function and std::any follow the same plan · Practise: many types behind one wrapper. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Explain the concept and model pattern behind type erasure
  • Build a type-erased wrapper with value semantics for unrelated types
  • Add a small-buffer optimisation and count the allocations it saves
  • Compare the small buffers of std::function and std::any on two standard libraries
  • Identify the failures that a type-erased wrapper hides, such as dangling references

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain the concept and model pattern behind type erasure
  • Build a type-erased wrapper with value semantics for unrelated types
  • Add a small-buffer optimisation and count the allocations it saves
  • Compare the small buffers of std::function and std::any on two standard libraries
  • Identify the failures that a type-erased wrapper hides, such as dangling references

Before you start

On this page

A drawing program must keep a Circle, a Label and a Grid from three different libraries in one std::vector, and the three classes share no base class. type erasure: Hiding the type of a stored object behind one ordinary wrapper type: the wrapper's constructor template makes a model of an internal interface for each type it is given, and every call goes through that interface, as in std::function and std::any. makes that possible: one ordinary, non-template type holds values of many different types, as long as each of them supports the operations the wrapper needs. std::function<int(int)> is the best-known example: the same std::function can hold a function pointer, a lambda or a function object, and std::any can hold a value of almost any copyable type.

Inside, the wrapper keeps a pointer to an internal interface, called the concept, and its constructor template creates an implementation of that interface, called a model, for each type it is given. A call goes through the interface to the model, and the model calls the stored object. Copying the wrapper copies the object inside, so it behaves like a value. The price is an indirect call on every use and, for larger objects, a heap allocation; every run below was made with Apple clang 21 and GCC 16.

Why not a base class, a template or a variant?

C++ already has three ways to treat different types alike, and each one leaves a gap. A base class with virtual function: A member function declared virtual in a base class. A derived class may override it with a function of the same name, parameters and const, and a call through a reference or a pointer to the base then runs the version of the object's own class. More in “Virtual functions, override and virtual destructors” needs every type to derive from it, which a type from another library cannot do. A function template accepts any type with the right member functions, but each type makes a different instantiation: The compiler writing a real function or class from a template for particular template arguments, such as larger<int> from larger, the first time a use needs it. Each instantiation is compiled as a function or class of its own. More in “C++ function templates and argument deduction”, so one vector cannot hold them all. A std::variant holds one of a list of types fixed in advance. Type erasure takes the open set of the base class, the “any type with the right functions” of the template, and the value semantics of the variant. The template can treat the types alike but never store them together, so the comparison that matters is between the other three:

Three ways to keep different types together
Criterion Base classVariantType erasure
Each type must Derive from the baseBe in the listHave the functions
Stored as Pointers to objectsValuesValues
New types later YesOnly by editing the listYes
A call goes through A virtual callstd::visitA virtual call inside

The C++ Core Guidelines describe the same idea as offering a generic interface on the outside while dispatching dynamically inside (rule T.5), and they also warn against overusing it (rule T.49): every call pays an indirection, so prefer a template where the type is known while compiling.

Checkpoint

Check: Base class, variant or type erasure

  1. Question 1 of 2 What does a std::variant need that type erasure does not?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Every type it can hold, listed in its own type

    A variant holds one of a list of types fixed in advance, so only its owner can add one. A type-erased wrapper accepts any type that has the functions it needs, including types written later.

  2. Question 2 of 2 Shapes come from three libraries you cannot change, more kinds will arrive later, and one std::vector must hold them as values. Which design fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A type-erased wrapper such as Drawable

    Type erasure combines the open set of a base class, the "any type with the right functions" of a template and the value semantics of a variant, which is exactly what this case asks for.

A concept and a model carry every call

Drawable holds any object that has a draw(std::ostream&) const member function. Its constructor template is the only place that knows the stored type: it wraps the object in a Model<T>, which implements the Concept interface, and from then on the type is erased:

A call to Drawable::draw goes through self_ to the Concept interface, then by a virtual call to Model<Label>, which calls Label::draw.The caller writes d.draw(out)and sees only the type DrawableDrawableholds one pointer, self_,to a ConceptConcept, an internal interface:virtual draw() and clone()Model<Label>, generated byDrawable's constructor template:holds a copy of the Label,its draw() calls label.draw()Label::draw()an ordinary member function;Label has no base classcalls draw()self_->draw(out)virtual call to the modelof the stored typeplain call

The path of one call through a type-erased wrapper

Text description of the diagram

The diagram is one column of five boxes joined by arrows, from the caller down to the stored object.

  1. The caller writes d.draw(out) and sees only the type Drawable. An arrow labelled "calls draw()" leads down.
  2. Drawable holds one pointer, self_, to a Concept. An arrow labelled "self_->draw(out)" leads down.
  3. Concept is an internal interface with the virtual functions draw() and clone(). An arrow labelled "virtual call to the model of the stored type" leads down.
  4. Model<Label> was generated by Drawable's constructor template; it holds a copy of the Label, and its draw() calls label.draw(). An arrow labelled "plain call" leads down.
  5. Label::draw() is an ordinary member function, and Label has no base class.
A type-erased Drawable, and three unrelated types in one vector

C header · drawable.h

// drawable.h: a type-erased Drawable. It holds a copy of any object that has a draw(std::ostream&) const member
// function, and gives it value semantics: copying a Drawable copies the object inside.
#pragma once
#include <concepts>
#include <memory>
#include <ostream>
#include <utility>

template <class T>
concept CanDraw = requires(const T& shape, std::ostream& out) { shape.draw(out); };

class Drawable {
public:
    template <CanDraw T>
        requires(!std::same_as<T, Drawable>)
    Drawable(T shape) : self_(std::make_unique<Model<T>>(std::move(shape))) {}

    Drawable(const Drawable& other) : self_(other.self_->clone()) {}
    Drawable& operator=(const Drawable& other) {
        self_ = other.self_->clone();                  // copy first: if that throws, *this is unchanged
        return *this;
    }
    Drawable(Drawable&&) noexcept = default;
    Drawable& operator=(Drawable&&) noexcept = default;

    void draw(std::ostream& out) const { self_->draw(out); }

private:
    struct Concept {                                   // the concept: what every stored type must offer
        virtual ~Concept() = default;
        virtual void draw(std::ostream& out) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    template <class T>
    struct Model final : Concept {                     // a model: how one type T offers it
        explicit Model(T s) : shape(std::move(s)) {}
        void draw(std::ostream& out) const override { shape.draw(out); }
        std::unique_ptr<Concept> clone() const override { return std::make_unique<Model>(*this); }
        T shape;
    };

    std::unique_ptr<Concept> self_;
};

C++ · shapes.cpp

// shapes.cpp: three unrelated types in one vector, with no common base class, and copies that stay independent.
#include "drawable.h"
#include <iostream>
#include <string>
#include <vector>

struct Circle {
    double radius;
    void draw(std::ostream& out) const { out << "circle, radius " << radius; }
};

struct Label {
    std::string text;
    void draw(std::ostream& out) const { out << "label \"" << text << '"'; }
};

class Grid {                                       // could come from another library: it knows nothing of Drawable
public:
    Grid(int rows, int columns) : rows_(rows), columns_(columns) {}
    void draw(std::ostream& out) const { out << "grid, " << rows_ << " x " << columns_ << " cells"; }

private:
    int rows_;
    int columns_;
};

int main() {
    std::vector<Drawable> scene{Circle{2.5}, Label{"Exit"}, Grid{3, 4}};
    for (const Drawable& d : scene) {
        d.draw(std::cout);
        std::cout << '\n';
    }

    Label sign{"Platform 3"};
    Drawable a = sign;                             // a holds its own copy of sign
    Drawable b = a;                                // b holds a copy of a's Label
    sign.text = "Platform 4";
    a = Circle{1.0};                               // a now holds a Circle; b keeps the Label
    std::cout << "a: ";
    a.draw(std::cout);
    std::cout << "\nb: ";
    b.draw(std::cout);
    std::cout << "\nsizeof(Drawable) = " << sizeof(Drawable) << " bytes\n";
}
Three types behind one wrapper, and two independent copies Python · shapes_session.py
from session import run

# Three unrelated types behind one Drawable, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra shapes.cpp -o shapes && ./shapes
g++-16 -std=c++23 -Wall -Wextra shapes.cpp -o shapes-gcc && ./shapes-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra shapes.cpp -o shapes && ./shapes
circle, radius 2.5
label "Exit"
grid, 3 x 4 cells
a: circle, radius 1
b: label "Platform 3"
sizeof(Drawable) = 8 bytes
$ g++-16 -std=c++23 -Wall -Wextra shapes.cpp -o shapes-gcc && ./shapes-gcc
circle, radius 2.5
label "Exit"
grid, 3 x 4 cells
a: circle, radius 1
b: label "Platform 3"
sizeof(Drawable) = 8 bytes

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

Circle, Label and Grid share no base class and know nothing about Drawable, yet one vector holds all three. The copy constructor calls clone(), which copies the stored object. Step through the last lines of shapes.cpp and watch which model each line changes:

Interactive · step by step Two Drawables that copy like values Step through the last lines of shapes.cpp. The left column is each variable, the right column the model on the heap that a Drawable owns; watch which model each line changes.
Two Drawables that copy like values, step 6 of 6. Showing Label sign ("Platform 4"), Drawable a, Drawable b, b's Model<Label>, a's Model<Circle>. In focus: Drawable a, Drawable b.self_self_Label sign"Platform 4"Drawable aone pointerDrawable bone pointerb's Model<Label>a copy: "Platform 3"a's Model<Circle>radius 1

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

Every step as text
  1. An ordinary Label. Label sign{"Platform 3"} is a plain struct with a draw() function. It knows nothing about Drawable and derives from nothing.
  2. Drawable a = sign;. The constructor template, the one place that knows T is Label, copies sign into a new Model<Label> on the heap and points a's self_ at it. From here on, a sees only the Concept interface.
  3. Drawable b = a;. The copy constructor calls self_->clone(). The virtual call reaches Model<Label>::clone, which copies the whole model, so b owns a second Label of its own.
  4. sign changes. sign.text = "Platform 4" changes sign alone. Both models hold their own copies, which still say "Platform 3".
  5. a = Circle{1.0};. A Drawable is built from the Circle and moved into a. a's old Model<Label> is destroyed, and its self_ now points to a Model<Circle>. b and its Label are untouched.
  6. Two independent values. a.draw prints "circle, radius 1" and b.draw prints the label "Platform 3". Each Drawable is 8 bytes on both compilers, one pointer, whatever it holds.

b kept the Label “Platform 3” after sign changed and after a was given a Circle. A Drawable is 8 bytes on both compilers, one pointer, whatever object it holds. The CanDraw constraint rejects a type without draw() at the line that stores it, instead of deep inside Model.

Key idea

Only the constructor template knows the stored type: it builds a model of the interface for that type, every call goes through the interface, and copying the wrapper clones the model.

Checkpoint

Check: The concept and model pattern

  1. Question 1 of 2 Which part of Drawable is a template over the stored type?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The constructor, and the Model<T> it creates

    Only the constructor is a template. It is the one place that knows the stored type, and it creates the Model<T> that implements the Concept for that type; after it, the type is erased.

  2. Question 2 of 2 A Drawable holds a Label whose text is 200 characters long. What does sizeof(Drawable) print?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 8 bytes

    A Drawable is one std::unique_ptr to its Concept, 8 bytes on both compilers. The Label, with its 200 characters, lives in the model on the heap.

std::function and std::any follow the same plan

std::function works like Drawable: its constructor template builds a model for the callable’s type, its call operator forwards through the interface, and copying it copies the callable. The standard wrappers add one refinement that matters for speed. A small-buffer optimisation: Keeping a small stored object inside the wrapper's own storage instead of on the heap, so that holding it needs no allocation. The standard recommends it for std::function and std::any; each library chooses the size of the buffer. stores small objects inside the wrapper itself, so that holding them needs no heap allocation. The standard recommends it for std::function and std::any and leaves the size of the buffer to each library; for std::any it allows the buffer only for types whose move constructor cannot throw. The newer callable wrappers differ in what they accept. std::move_only_function (C++23) is moved, never copied, so it also holds callables that cannot be copied. std::copyable_function (C++26) is the copyable counterpart of std::move_only_function, and std::function_ref (C++26) refers to a callable instead of owning it. Walk through the editions:

Timeline · 4 events in order The standard library's type-erased wrappers, edition by edition Walk through the editions. Each wrapper keeps the concept and model inside and differs in what it accepts and whether it owns it.
  1. C++11

    std::function, and the deleter of std::shared_ptr

    std::function<R(Args...)> holds a copy of any copyable callable with that signature. A std::shared_ptr hides its deleter the same way: the deleter is not part of the shared_ptr's type.

  2. C++17

    std::any

    std::any holds a copy of a value of almost any copyable type. Its small buffer may hold only types whose move constructor cannot throw.

  3. C++23

    std::move_only_function moves only

    Like std::function, it holds a callable behind one type, and it also accepts callables that cannot be copied (paper P0288R9).

  4. C++26

    std::copyable_function and std::function_ref new wrappers

    std::copyable_function is the copyable counterpart of std::move_only_function (paper P2548R6). std::function_ref refers to a callable instead of owning it.

The timeline has 4 events, from C++11 to C++26.

Checkpoint

Check: The standard type-erased wrappers

  1. Question 1 of 2 What does std::function's constructor template do, in the terms of the concept and model pattern?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It builds a model for the callable's type, behind an interface for the signature

    std::function follows the same plan as Drawable: its constructor template builds a model for the callable's type, its call operator forwards through the interface, and copying it copies the callable.

  2. Question 2 of 2 A callable cannot be copied, only moved. Which of these standard wrappers can hold it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::move_only_function

    std::function, std::copyable_function and std::any copy what they hold, so they need copyable contents. std::move_only_function (C++23) also accepts callables that can only be moved.

Practise: many types behind one wrapper

Ten questions on these three ideas, asked one at a time. The idea your answers show is weakest comes first, a right answer brings a harder question, and the reason behind each choice appears as soon as you pick it.

Practice that adapts to you

Practise this lesson

10 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 10 Why can a base class with a virtual draw() not hold the Grid from another library?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: Grid would have to derive from the base, and its code cannot be changed

    A base class needs every stored type to derive from it. Grid comes from another library and knows nothing about the interface, so only a wrapper that needs no base class can take it.

  2. Question 2 of 10 A function template draw_all(const T&) accepts any type with draw(). Why can it not replace Drawable in the scene vector?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Each type makes a different instantiation, so no single element type covers them all

    A template can treat the types alike but never store them together: std::vector needs one element type, and draw_all<Circle> and draw_all<Label> are different functions. Type erasure gives that one type.

  3. Question 3 of 10 Rule T.49 of the C++ Core Guidelines says to avoid type erasure where possible. When is a template the better choice?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: When the type is known while compiling, since every call through the wrapper pays an indirection

    The Guidelines offer type erasure as a generic interface outside with dynamic dispatch inside (T.5), and warn that every call pays an indirection (T.49). Where the type is known while compiling, a template is simpler and faster.

  4. Question 4 of 10 What does type erasure let one ordinary, non-template type do?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Hold values of many unrelated types that support the same operations

    A type-erased wrapper such as std::function or the lesson's Drawable has one type of its own, yet it can hold a lambda, a function object or a Circle, as long as the stored type supports the operations the wrapper needs. A fixed list of types is what std::variant holds.

  5. Question 5 of 10 In the concept-and-model pattern, which part is a template?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: The model, generated by the wrapper's constructor template for each stored type

    The concept is a plain abstract class, and the wrapper is an ordinary class. Only the constructor is a template: it is the one place that knows the stored type, and it creates the Model<T> that implements the concept for that type.

  6. Question 6 of 10 Drawable is the lesson's wrapper. What does the last line print?

    Read the code, then choose one answer.

    Label sign{"Gate 4"};
    Drawable a = sign;
    Drawable b = a;
    sign.text = "Gate 5";
    a = Circle{2.0};
    b.draw(std::cout);
    Show the answer: Question 6 of 10

    Answer: label "Gate 4"

    Drawable has value semantics. a holds its own copy of sign, and copying a into b clones that copy, so later changes to sign and to a do not reach b.

  7. Question 7 of 10 d.draw(out) is called on a Drawable that holds a Grid. In which order are the functions reached?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Drawable::draw, then Model<Grid>::draw through a virtual call, then Grid::draw

    The caller sees only Drawable. Drawable calls self_->draw(out), a virtual call that reaches Model<Grid>::draw, which makes a plain call to the stored Grid's own draw().

  8. Question 8 of 10 What does a small-buffer optimisation in a wrapper save?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: The heap allocation for objects small enough to fit inside the wrapper

    A small-buffer optimisation stores small objects inside the wrapper itself, so that holding them needs no heap allocation. The standard recommends it for std::function and std::any.

  9. Question 9 of 10 Which of these hide the type of what they store from their own type?

    Choose every answer that is right.

    Show the answer: Question 9 of 10

    Answer:

    • std::any
    • std::function<void()>
    • The deleter of a std::shared_ptr<T>

    std::function and std::any can hold types that appear nowhere in their own type, and a shared_ptr's deleter is not part of the shared_ptr's type either. A variant lists every type it can hold, and a vector holds one known element type.

  10. Question 10 of 10 Why must a callable be copyable to go into a std::function, but not to go into a std::move_only_function?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Copying a std::function copies the callable inside it, and a std::move_only_function is never copied

    A wrapper that can be copied must copy what it holds, as Drawable's clone() copies its model, so it needs a copyable callable. std::move_only_function can only be moved, so it accepts callables that can only be moved.

A small buffer inside the wrapper, measured on both libraries Pro: opens with a Pro, Premium or Ultimate pass

Most stored objects are a few bytes, so a heap allocation for each of them costs more than the object itself. This part builds a 32-byte buffer into Drawable, counts the allocations it saves, and measures the buffers that std::function and std::any already have.

In this part:

  • Storing small objects inside the wrapper, and the three conditions
  • 1,000 copies of six shapes, counted
  • What std::function and std::any keep inside, measured

1 diagram · 5 runnable examples (C header, C++, Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Hidden references and empty wrappers, reproduced, then an exercise Pro: opens with a Pro, Premium or Ultimate pass

A type-erased wrapper hides what it holds, including references that outlive what they refer to. This part reproduces that failure under AddressSanitizer, shows what the standard wrappers do when they are empty or asked for the wrong type, compares the standard wrappers, and ends with an exercise.

In this part:

  • A std::function that keeps a dangling reference, caught by AddressSanitizer
  • Empty wrappers and wrong casts: what each one throws
  • Choosing a standard wrapper, with what each library has
  • Your turn: AnyPrintable with a 32-byte buffer

1 diagram · 4 runnable examples (C header (any_printable.h), C header (count_new.h), C++, Python) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

One warm-up, answered in full, then three design questions:

  1. Fresher to mid · Warm-up

    What is type erasure in C++? Name two standard library types that use it.

    Type erasure lets one ordinary, non-template type hold values of many different types that support the same operations, without those types sharing a base class. The wrapper keeps a pointer to an internal abstract class, the concept, and its constructor template creates an implementation of it, a model, for each type it is given; calls go through the concept to the model, which calls the stored object. std::function is the classic example: std::function<int(int)> can hold a function pointer, a lambda or a function object. std::any holds a copy of a value of almost any copyable type, and the deleter of a std::shared_ptr is erased too, since it is not part of the shared_ptr's type. The price is an indirect call on each use and, for objects too big for an internal buffer, a heap allocation.

  2. Mid to senior

    A thread pool stores queued jobs as std::function<void()>. A new job type captures a std::unique_ptr, and the code stops compiling. Explain why, and choose a type for the queue that works with Apple clang's libc++ and with GCC's libstdc++.

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

  3. Senior to staff

    A profile of an event loop shows millions of heap allocations a second inside std::function. How do you tell which callables allocate, and what are your options for removing those allocations?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Design a Shape value type that can hold any shape class from several libraries, copies like an int and can be stored in a std::vector. Walk through its interface, its copy and move operations, and how you would keep copies cheap.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A base class needs every type to derive from it, a template cannot store different types together and a variant lists its types in advance; type erasure keeps an open set of types, stored as values.
  • Type erasure gives one ordinary type value semantics over many unrelated types: a concept interface, a model template per stored type, and a constructor template that connects them.
  • std::function, std::any and the deleter of std::shared_ptr are type-erased; std::variant is not, because it names every type it can hold.
  • A small buffer keeps small objects inside the wrapper without an allocation; each library sizes its own, and std::any uses it only for types whose move cannot throw.
  • Each call goes through an indirection and each stored object may need a heap allocation, so prefer a template where the type is known while compiling.

Terms in this lesson

small-buffer optimisation
Keeping a small stored object inside the wrapper's own storage instead of on the heap, so that holding it needs no allocation. The standard recommends it for std::function and std::any; each library chooses the size of the buffer.
type erasure
Hiding the type of a stored object behind one ordinary wrapper type: the wrapper's constructor template makes a model of an internal interface for each type it is given, and every call goes through that interface, as in std::function and std::any.

References

Related tools

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.