The visitor pattern
A virtual function chooses an implementation from the dynamic type of one object. The visitor pattern chooses from the dynamic type of the object and the static type of the operation at once, which is called double dispatch.
The cost is a class that every element has to know: accept. The benefit is that a new operation is a new visitor class, and the elements of the hierarchy do not change. The standard alternative for a closed set of types is std::variant with std::visit, which does the same dispatch without a base class.
The pattern suits a hierarchy that is stable in its types, and a variant suits a closed set that is stable in its operations. A project picks one and keeps it, because mixing them doubles the places a new shape has to be added.
accept and visit
The shape base declares accept(visitor&), and each derived class implements it by calling visitor.visit(*this). The static type at that call is the derived class, so the right overload of visit is chosen.
The program sums the area of a circle and a square through one visitor. The result is 7, rounded down from 7.14, and the loop knows only shape.
A new operation is a new visitor with two overrides, and nothing in the shapes changes. A new shape, on the other hand, changes the visitor interface, which is the trade the pattern makes: it is open for operations and closed for types.
#include <iostream>
#include <memory>
#include <vector>
class circle;
class square;
class visitor
{
public:
virtual ~visitor() = default;
virtual void visit(const circle& shape) = 0;
virtual void visit(const square& shape) = 0;
};
class shape
{
public:
virtual ~shape() = default;
virtual void accept(visitor& v) const = 0;
};
class circle : public shape
{
public:
explicit circle(double radius) : radius_(radius) {}
void accept(visitor& v) const override { v.visit(*this); }
double radius() const { return radius_; }
private:
double radius_;
};
class square : public shape
{
public:
explicit square(double side) : side_(side) {}
void accept(visitor& v) const override { v.visit(*this); }
double side() const { return side_; }
private:
double side_;
};
class area : public visitor
{
public:
void visit(const circle& shape) override
{
total += 3.14159 * shape.radius() * shape.radius();
}
void visit(const square& shape) override
{
total += shape.side() * shape.side();
}
double total = 0;
};
int main()
{
std::vector<std::unique_ptr<shape>> shapes;
shapes.push_back(std::make_unique<circle>(1.0));
shapes.push_back(std::make_unique<square>(2.0));
area visitor;
for (const auto& shape : shapes) {
shape->accept(visitor);
}
std::cout << static_cast<int>(visitor.total) << '\n';
}
g++ -std=c++23 -Wall -Wextra double.cpp -o double_dispatch
./double_dispatch
7
std::visit over a variant
A std::variant<circle, square> holds one of a closed set of types, and std::visit calls the function with the alternative that is stored. No base class and no accept are needed, and the set of types is visible in the declaration.
The program builds a vector of variants and sums the areas with one generic lambda. The if constexpr chooses the formula at compile time, and the output is the same 7.
The trade is the opposite of the class hierarchy: adding a type means editing every visitor, and adding an operation means editing the one lambda. A variant is also a value, so it copies and compares without an allocation.
#include <iostream>
#include <type_traits>
#include <variant>
#include <vector>
struct circle
{
double radius;
};
struct square
{
double side;
};
int main()
{
std::vector<std::variant<circle, square>> shapes{circle{1.0}, square{2.0}};
double total = 0;
for (const auto& shape : shapes) {
total += std::visit([](const auto& value) {
if constexpr (std::is_same_v<std::decay_t<decltype(value)>, circle>) {
return 3.14159 * value.radius * value.radius;
} else {
return value.side * value.side;
}
}, shape);
}
std::cout << static_cast<int>(total) << '\n';
}
g++ -std=c++23 -Wall -Wextra variant.cpp -o variant_visit
./variant_visit
7
A visitor that reports
A visitor does not have to return a number. The one below prints a line per shape, and the two overloads are the two kinds of output the program can produce.
The program describes one circle and one square and prints their measurements. The dynamic type of the shape and the static type of the visitor together choose the line, which is the double dispatch in its simplest visible form.
The same shape is reusable with the area visitor of the first section, and the two operations do not know about each other. That separation is what makes the pattern worth its boilerplate in a hierarchy that changes rarely.
#include <iostream>
#include <memory>
#include <vector>
class circle;
class square;
class visitor
{
public:
virtual ~visitor() = default;
virtual void visit(const circle& shape) = 0;
virtual void visit(const square& shape) = 0;
};
class shape
{
public:
virtual ~shape() = default;
virtual void accept(visitor& v) const = 0;
};
class circle : public shape
{
public:
explicit circle(double radius) : radius_(radius) {}
void accept(visitor& v) const override { v.visit(*this); }
double radius() const { return radius_; }
private:
double radius_;
};
class square : public shape
{
public:
explicit square(double side) : side_(side) {}
void accept(visitor& v) const override { v.visit(*this); }
double side() const { return side_; }
private:
double side_;
};
class describe : public visitor
{
public:
void visit(const circle& shape) override
{
std::cout << "circle " << shape.radius() << '\n';
}
void visit(const square& shape) override
{
std::cout << "square " << shape.side() << '\n';
}
};
int main()
{
std::vector<std::unique_ptr<shape>> shapes;
shapes.push_back(std::make_unique<circle>(1.5));
shapes.push_back(std::make_unique<square>(2.0));
describe visitor;
for (const auto& shape : shapes) {
shape->accept(visitor);
}
}
g++ -std=c++23 -Wall -Wextra describe.cpp -o describe_shapes
./describe_shapes
circle 1.5
square 2
The alternative no visitor handles
std::visit requires the function to accept every alternative of the variant. A visitor with one overload and a variant of two types fails to compile, and the message is about a call with no matching function.
The program passes a visitor that only knows circle to a variant that can also hold square. The compiler lists the instantiations it tried, and the last line names the type that had no handler.
The fix is the second overload, or a generic lambda that covers both. A visitor hierarchy with a pure virtual for each type turns the same mistake into an error at the class definition, which is the earlier and clearer place to catch it.
#include <variant>
struct circle
{
double radius;
};
struct square
{
double side;
};
struct only_circle
{
void operator()(const circle&) const {}
};
int main()
{
std::variant<circle, square> shape{square{2.0}};
std::visit(only_circle{}, shape);
}
g++ -std=c++23 -Wall -Wextra missing.cpp -o missing
In file included from missing.cpp:1:
/usr/include/c++/15.2.0/variant: In instantiation of 'constexpr bool std::__detail::__variant::__check_visitor_results(std::index_sequence<_Ind ...>) [with _Visitor = only_circle; _Variant = std::variant<circle, square>&; long unsigned int ..._Idxs = {0, 1}; std::index_sequence<_Ind ...> = std::integer_sequence<long unsigned int, 0, 1>]':
/usr/include/c++/15.2.0/variant:1944:44: required from 'constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = only_circle; _Variants = {variant<circle, square>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = void]'
1943 | constexpr bool __visit_rettypes_match = __detail::__variant::
| ~~~~~~~~~~~~~~~~~~~~~
1944 | __check_visitor_results<_Visitor, _Vp>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
1945 | make_index_sequence<variant_size_v<remove_reference_t<_Vp>>>());
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
missing.cpp:22:15: required from here
22 | std::visit(only_circle{}, shape);
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1129:14: error: no type named 'type' in 'struct std::invoke_result<only_circle, square&>'
1129 | return __same_types<
| ^~~~~~~~~~~~~
1130 | invoke_result_t<_Visitor, __get_t<_Idxs, _Variant>>...
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1131 | >;
| ~
/usr/include/c++/15.2.0/variant: In instantiation of 'constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = only_circle; _Variants = {variant<circle, square>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = void]':
missing.cpp:22:15: required from here
22 | std::visit(only_circle{}, shape);
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1948:29: error: non-constant condition for static assertion
1948 | static_assert(__visit_rettypes_match,
| ^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant: In instantiation of 'static constexpr decltype(auto) std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...)>, std::integer_sequence<long unsigned int, __indices ...> >::__visit_invoke(_Visitor&&, _Variants ...) [with _Result_type = std::__detail::__variant::__deduce_visit_result<void>; _Visitor = only_circle&&; _Variants = {std::variant<circle, square>&}; long unsigned int ...__indices = {1}]':
/usr/include/c++/15.2.0/variant:1892:5: required from 'constexpr decltype(auto) std::__do_visit(_Visitor&&, _Variants&& ...) [with _Result_type = __detail::__variant::__deduce_visit_result<void>; _Visitor = only_circle; _Variants = {variant<circle, square>&}]'
1892 | _GLIBCXX_VISIT_CASE(1)
| ^~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1954:34: required from 'constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = only_circle; _Variants = {variant<circle, square>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = void]'
1954 | return std::__do_visit<_Tag>(
| ~~~~~~~~~~~~~~~~~~~~~^
1955 | std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1956 | static_cast<_Vp>(__variants)...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
missing.cpp:22:15: required from here
22 | std::visit(only_circle{}, shape);
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1055:31: error: no matching function for call to '__invoke(only_circle, square&)'
1055 | return std::__invoke(std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1056 | __element_by_index_or_cookie<__indices>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1057 | std::forward<_Variants>(__vars))...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1055:31: note: there is 1 candidate
In file included from /usr/include/c++/15.2.0/variant:47:
/usr/include/c++/15.2.0/bits/invoke.h:92:5: note: candidate 1: 'template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...)'
92 | __invoke(_Callable&& __fn, _Args&&... __args)
| ^~~~~~~~
/usr/include/c++/15.2.0/bits/invoke.h:92:5: note: template argument deduction/substitution failed:
/usr/include/c++/15.2.0/bits/invoke.h: In substitution of 'template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...) [with _Callable = only_circle; _Args = {square&}]':
/usr/include/c++/15.2.0/variant:1055:24: required from 'static constexpr decltype(auto) std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...)>, std::integer_sequence<long unsigned int, __indices ...> >::__visit_invoke(_Visitor&&, _Variants ...) [with _Result_type = std::__detail::__variant::__deduce_visit_result<void>; _Visitor = only_circle&&; _Variants = {std::variant<circle, square>&}; long unsigned int ...__indices = {1}]'
1055 | return std::__invoke(std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1056 | __element_by_index_or_cookie<__indices>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1057 | std::forward<_Variants>(__vars))...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1892:5: required from 'constexpr decltype(auto) std::__do_visit(_Visitor&&, _Variants&& ...) [with _Result_type = __detail::__variant::__deduce_visit_result<void>; _Visitor = only_circle; _Variants = {variant<circle, square>&}]'
1892 | _GLIBCXX_VISIT_CASE(1)
| ^~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1954:34: required from 'constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = only_circle; _Variants = {variant<circle, square>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = void]'
1954 | return std::__do_visit<_Tag>(
| ~~~~~~~~~~~~~~~~~~~~~^
1955 | std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1956 | static_cast<_Vp>(__variants)...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
missing.cpp:22:15: required from here
22 | std::visit(only_circle{}, shape);
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/invoke.h:92:5: error: no type named 'type' in 'struct std::__invoke_result<only_circle, square&>'
92 | __invoke(_Callable&& __fn, _Args&&... __args)
| ^~~~~~~~
Exercises
- Add a rectangle to the variant program and make the generic lambda handle it.
- Write a visitor that returns the name of the shape as a std::string and call it on the vector.
- Add a third shape to the class hierarchy and count how many places the compiler now reports.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does double dispatch mean in the visitor pattern?
Answer
The operation is chosen from the dynamic type of the element and the type of the visitor
accept is virtual on the element, and the call to visit selects the overload from the visitor's static type.
-
What does every element of a visitor hierarchy have to provide?
Answer
An accept method that calls visit with its own type
The accept method is what supplies the second half of the dispatch, with
*thisof the derived type. -
What is the advantage of the pattern?
Answer
A new operation is a new visitor, and the element classes do not change
The hierarchy stays closed for new types and open for new operations, which is the trade it makes.
-
What does this program print?
#include <iostream> struct circle { double radius; }; struct square { double side; }; struct visitor { double total = 0; void operator()(const circle& c) { total += 3.14159 * c.radius * c.radius; } void operator()(const square& s) { total += s.side * s.side; } }; struct shape { virtual ~shape() = default; virtual void accept(visitor&) const = 0; }; struct circle_shape : shape { explicit circle_shape(double r) : radius(r) {} void accept(visitor& v) const override { v(circle{radius}); } double radius; }; struct square_shape : shape { explicit square_shape(double s) : side(s) {} void accept(visitor& v) const override { v(square{side}); } double side; }; int main() { circle_shape c{1.0}; square_shape s{2.0}; visitor v; c.accept(v); s.accept(v); std::cout << static_cast<int>(v.total); }Answer
7
The circle adds 3.14 and the square 4, and the total is truncated to an int.
-
What does this program print?
#include <iostream> #include <type_traits> #include <variant> #include <vector> struct circle { double radius; }; struct square { double side; }; int main() { std::vector<std::variant<circle, square>> shapes{circle{1.0}, square{2.0}}; double total = 0; for (const auto& shape : shapes) total += std::visit([](const auto& value) { if constexpr (std::is_same_v<std::decay_t<decltype(value)>, circle>) return 3.14159 * value.radius * value.radius; else return value.side * value.side; }, shape); std::cout << static_cast<int>(total); }Answer
7
The same two areas are summed through std::visit, with the same result.
-
What does this program print?
#include <iostream> struct circle { double radius; }; struct square { double side; }; struct describe { void operator()(const circle& c) const { std::cout << "circle " << c.radius << '\n'; } void operator()(const square& s) const { std::cout << "square " << s.side << '\n'; } }; int main() { describe d; d(circle{1.5}); d(square{2.0}); }Answer
circle 1.5 / square 2
Each overload prints its own name and measurement.
-
What happens when a visitor lacks an overload for one alternative of a variant?
Answer
std::visit does not compile: no matching call
visit instantiates the call for every alternative, and the missing one is a compile error.
-
How do you add a type to a variant-based visitor?
Answer
Add it to the variant's type list and handle it in every visit
The variant's alternatives are the closed set, and every visitor must compile for all of them.
-
Which is true of the class-based visitor when a new shape is added?
Answer
The visitor interface grows and every visitor must implement the new overload
A new element type is the case the pattern handles worst, and the compiler reports each visitor.
-
A generic lambda visits a
std::variant<circle, square>. Why isif constexprused inside it?Answer
To choose the formula for each alternative at compile time
The lambda is instantiated per alternative, and if constexpr discards the branch that would not compile.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev