Visitor Pattern in Modern C++

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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s