The Visitor Pattern is a well-known technique of adding specific functionality to a number of possibly unrelated classes without actually modifying them significantly. If you do an online search for examples of this design pattern in C++, you may find examples with an abstract Visitor
base class having a single, overloaded, pure virtual function, often called accept()
. This article explores how to accomplish much the same task using compile-time polymorphism and more recent C++ syntax.
Firstly, we need to define the Visitor
base class and a type deduction guide (the latter of which would appear to not be needed by MSVC or g++ in C++20 mode, but is in C++17 mode):
template<typename... Base>
struct Visitor : Base... {
using Base::operator()...;
};
template<typename...T> Visitor(T...) -> Visitor<T...>;
If your understanding of template classes doesn’t (yet) generalize to use of ellipses, don’t worry! All they mean in this context is that Visitor
inherits from any number of Base
classes and exposes their (overloaded) function call operators. Traditionally in C++, having an overloaded function call operator would mean “functor”, however in more modern times we often think “callable” equals “lambda function”. Here is how to initialize an instance of Visitor
:
constexpr Visitor visitor{
[](double d){ return d + 3.4; },
[](int i){ return i / 2.0; }
};
As shown above, the uniform initialization constructor takes any number of comma-separated (pure) lambdas which accept different types of parameter thus overloading the function call operator of Visitor
. Note that the return type (explicit or implied) must be identical for each of these lambda functions, or a compile-time error will be generated.
The magic happens with use of std::visit
(from header <variant>
), which takes two parameters: a visitor as defined above and a std::variant
with the same types as the overloaded call operators. In the example code below we’ve included a using
declaration but this is not strictly necessary:
int main() {
using Visitable = std::variant<double,int>;
constexpr auto result1 = std::visit(visitor, Visitable(9.2));
constexpr auto result2 = std::visit<int>(visitor, Visitable(5));
std::cout << "result1 = " << result1 << "\nresult2 = " << result2 << '\n';
}
The result from std::visit
can be applied to a constexpr
variable, thus proving that we can evaluate the visitor at compile-time as well as run-time. Also, the type returned can optionally be specified as a template type parameter to std::visit
, so even though we can’t return i / 2;
(as an int
) we can specify the cast that should be used, as in std::visit<int>()
. The output from running this program is:
result1 = 12.6
result2 = 2
Of course, this technique becomes more valuable with user-defined types. The following example program was inspired by Derek Banas’ Java video tutorial on the visitor pattern; three good classes (Liquor
, Tobacco
and Necessity
) are defined with different ways of storing and retrieving their cost (as a double
). Then, two different visitors are defined which apply different taxation schemes (the first is 50% on liquor, 80% on tobacco, and zero tax on necessities, the second is zero on all). Note the use of a generic lambda in TaxFreeVisitor
as a catch-all for those goods with operator double()
defined. A main()
program applies the visitors to each good in turn and prints out the costs-after-tax:
#include <iostream>
#include <variant>
template<typename... Base>
struct Visitor : Base... {
using Base::operator()...;
};
template<typename...T> Visitor(T...) -> Visitor<T...>;
class Liquor {
double price;
public:
Liquor(double price) : price{ price } {}
operator double() const { return price; }
};
class Tobacco {
double item;
public:
Tobacco(double itemPrice) : item{ itemPrice } {}
double getItemPrice() const { return item; }
};
class Necessity {
unsigned costInCents;
public:
Necessity(double cost) : costInCents{ static_cast<unsigned>(cost * 100) } {}
operator double() const { return costInCents / 100.0; }
};
Visitor TaxVisitor{
[](const Liquor& liquor){ double l = liquor; return l * 1.5; },
[](const Tobacco& tobacco) { return tobacco.getItemPrice() * 1.8; },
[](const Necessity& necessity) { double n = necessity; return n * 1.0; }
};
Visitor TaxFreeVisitor{
[](const Tobacco& tobacco) { return tobacco.getItemPrice() * 1.0; },
[](const auto& any) { double price = any; return price * 1.0; }
};
int main() {
using Visitable = std::variant<Liquor,Tobacco,Necessity>;
Liquor whiskey{ 25.99 };
Tobacco cigs{ 13.49 };
Necessity bread{ 1.29 };
std::cout.precision(2);
std::cout << "Many Items Taxable\n" << std::fixed;
std::cout << " Whiskey: $" << std::visit(TaxVisitor, Visitable(whiskey)) << '\n';
std::cout << " Cigarettes: $" << std::visit(TaxVisitor, Visitable(cigs)) << '\n';
std::cout << " Bread: $" << std::visit(TaxVisitor, Visitable(bread)) << '\n';
std::cout << "All Items Tax-Free\n";
std::cout << " Whiskey: $" << std::visit(TaxFreeVisitor, Visitable(whiskey)) << '\n';
std::cout << " Cigarettes: $" << std::visit(TaxFreeVisitor, Visitable(cigs)) << '\n';
std::cout << " Bread: $" << std::visit(TaxFreeVisitor, Visitable(bread)) << '\n';
}
Output from running this program:
Many Items Taxable
Whiskey: $38.98
Cigarettes: $24.28
Bread: $1.29
All Items Tax-Free
Whiskey: $25.99
Cigarettes: $13.49
Bread: $1.29
The above example program tries to show use of the visitor pattern where modifying the classes themselves is not either possible or not desirable, for example the need to add a tax field to each good and modify it in turn may become a maintenance headache with many goods and different taxation schemes. The Visitor
class definition is unchanged from before, and the TaxVisitor
and TaxFreeVisitor
objects are created from pure lambdas in the same way as the first visitor
we showed. (Capturing lambdas could be employed here, for example to avoid hard coding the tax bands, if TaxVisitor
were defined inside a function.) You should now be equipped to use the Visitor Pattern in your own C++ programs, using a very small amount of boilerplate and std::visit
from the C++ Standard Library.