Defining Visitors Inline in Modern C++

Defining Visitors Inline in Modern C++

By Robert Mill, Jonathan Coe

Overload, 22(123):17-19, October 2014


The Visitor pattern can involve non-local boilerplate code. Robert Mill and Jonathan Coe present an inline Visitor in C++.

The Visitor pattern can be useful when type-specific handling is required and tight coupling of type-handling logic and handled types is either an acceptable cost or desirable in its own right. We’ve found that selective application of the classical Visitor pattern adds strong compile-time safety, as the handling of new types needs explicit consideration in every context where type-specific handling occurs. The Visitor pattern presents an inversion of control that can feel unnatural and often requires introduction of considerable non-local boilerplate code. We’ve found that this slows adoption of the Visitor pattern especially among engineers and scientists who traditionally write their type-handling logic inline. Here we present a solution for defining Visitor s inline.

The problem

In object-oriented programming, we may need to perform a function on an object of polymorphic type, such that the behaviour of the function is specific to the derived type. Suppose that for the abstract base class Polygon we derive the concrete classes Triangle and Square . The free function CountSides , returns the number of sides in the polygon, p (see Listing 1).

struct Triangle : Polygon
{
  // members
}

struct Square : Polygon
{
  // members
}

int CountSides(Polygon& p)
{
  // implementation
}
			
Listing 1

CountSides will need the derived type of the polygon p to compute its result, which is problematic, because its argument is conveyed by a reference of the base class type, Polygon .

Visitor pattern

The Visitor design pattern offers a mechanism for type-specific handling using virtual dispatch [ Gamma95 ]. In the words of Scott Meyers: “ Visitor lets you define a new operation without changing the classes of the elements on which it operates ” [ Meyers06 ]. The pattern uses the this pointer inside the class to identify the derived type. Each derived object must accept a Visitor interface which provides a list of visit members with a single argument overloaded on various derived types. To continue our illustration, the PolygonVisitor is able to visit Triangle s and Square s, and all these polygons must be able to accept a PolygonVisitor . (See Listing 2.)

struct Triangle; 
struct Square;

struct PolygonVisitor 
{ 
  virtual ~PolygonVisitor() {}

  virtual void visit(Triangle& tr) = 0; 
  virtual void visit(Square& sq) = 0;
};

struct Polygon 
{ 
  virtual void accept(PolygonVisitor& v) = 0; 
}
			
Listing 2

Square s and Triangle s accept the Visitor as shown in Listing 3. Observe that the this pointer is used to select the appropriate overloaded function in the Visitor interface.

struct Triangle : Polygon 
{ 
  void accept(PolygonVisitor& v) override 
  { 
    v.visit(*this); 
  } 
};

struct Square : Polygon 
{ 
  void accept(PolygonVisitor& v) override 
  {
    v.visit(*this); 
  } 
};
			
Listing 3

A Visitor object, SideCounter , which counts the number of sides of a polygon and stores the result, is implemented and used as in Listing 4.

struct SideCounter : PolygonVisitor 
{ 
  void visit(Square& sq) override 
  { 
    m_sides = 4; 
  }
  
  void visit(Triangle& tr) override 
  { 
    m_sides = 3; 
  }
  
  int m_sides = 0; 
};

int CountSides(Polygon& p) 
{ 
  SideCounter sideCounter; 
  p.accept(sideCounter);
  return sideCounter.m_sides; 
}
			
Listing 4

Inline Visitor pattern

One potential drawback of the Visitor pattern is that it requires the creation of a new visitor object type for each algorithm that operates on the derived type. In some cases, the class created will not be reused and, much like a lambda, it would be more convenient to write the visitor clauses inline. Listing 5 shows how this can be accomplished in a form that resembles a switch statement.

int CountSides(Polygon& p) 
{ 
  int sides = 0;
  
  auto v = begin_visitor<PolygonVisitor>
    .on<Triangle>([&sides](Triangle& tr) 
    {
      sides = 3; 
    }) 
    .on<Square>([&sides](Square& sq) 
    { 
      sides = 4; 
    }) 
    .end_visitor();
  
  p.accept(v); 
  return sides; 
}
			
Listing 5

In Listing 6, we demonstrate generic code that permits the begin_visitor ... end_visitor construction to be used with any visitor base. The initial begin_visitor call instantiates a class which defines an inner object inheriting from the visitor interface; each subsequent call of the on function instantiates a class whose inner class inherits from the previous inner class implementing an additional visit function. Finally the end_visitor call returns an instance of the inner visitor class.

template <typename T,
          typename F,
          typename BaseInner,
          typename ArgsT>
struct ComposeVisitor
{
  struct Inner : public BaseInner
  {
    using BaseInner::visit;
    Inner(ArgsT&& args) :
      BaseInner(move(args.second)),
      m_f(move(args.first))
    {
    }
    void visit(T& t) final override
    {
      m_f(t);
    }
  private:
    F m_f;
  };
  ComposeVisitor(ArgsT&& args) :
    m_args(move(args))
  {
  }
  template <typename Tadd,
            typename Fadd>
  ComposeVisitor<
    Tadd,
    Fadd,
    Inner,
    pair<Fadd, ArgsT>> on(Fadd&& f)
  {
    return ComposeVisitor<
      Tadd,
      Fadd,
      Inner,
      pair<Fadd, ArgsT>>(
        make_pair(
          move(f),
          move(m_args)));
  }
  Inner end_visitor()
  {
    return Inner(move(m_args));
  }
  
  ArgsT m_args;
};
template <typename TVisitorBase>
struct EmptyVisitor
{
  struct Inner : public TVisitorBase
  {
    using TVisitorBase::visit;
    Inner(nullptr_t) {}
  };
  
  template <typename Tadd, typename Fadd>
  ComposeVisitor<
    Tadd,
    Fadd,
    Inner,
    pair<Fadd, nullptr_t>> on(Fadd&& f)
  {
    return ComposeVisitor<
      Tadd,
      Fadd,
      Inner,
      pair<Fadd, nullptr_t>>(
        make_pair(
          move(f),
          nullptr));
  }
};
template <typename TVisitorBase>
EmptyVisitor<TVisitorBase> begin_visitor()
{
  return EmptyVisitor<TVisitorBase>();
}
			
Listing 6

The consistency between the list of types used with on and those in the visitor base is verified at compilation time. Since the override qualifier is specified on the visit member function, it is not possible to add a superfluous visit which does not correspond to a type overload in the visitor base. Similarly, because the final qualifier is specified on the visit member function it is not possible to define a visit member function more than once. That inline visitors cannot be constructed when clauses are missing may also be considered desirable in some contexts. For instance, if a new type Hexagon is derived from Polygon , then the code base will compile only when appropriate visit functions been introduced to handle it. In large code bases, this may serve maintainability. If it is deemed that a visitor clause should have some default behaviour (e.g., no operation), a concrete visitor base can be passed into begin_visitor .

Performance

With optimizations turned on MSVC 2013, GCC 4.9.1 and Clang 3.4.2 compile the inline visitor without introducing any cost. GCC and Clang produce identical assembly code in the case when a visitor class is explicitly written out. MSVC produces different assembly code for the inline visitor and explicit visitor class; the inline visitor has been measured to run marginally faster.

Other visitors

Loki’s Acyclic Visitor [ Martin ] [ Loki ] removes compile-time coupling from visiting and visited classes but at the cost of introducing dynamic casts and run-time detection of unhandled types. When run-time performance and compile-time detection of unhandled types are favoured over shorter compile-times then we would recommend use of the inline visitor. The inline visitor does not have the flexibility of the Cooperative Visitor [ Krishnamoorthi07 ], which allows different method names and return types, but as it is intended to be lightweight this flexibility is not needed: the visit functions are not explicitly named and variables in local scope can be modified by lambda capture alleviating the need for a return value.

Conclusion

We have presented a method for defining inline visitors in standard C++. The method does not, by design, remove the tight coupling between visited and visiting class hierarchies. Performance, portability and convenience of the inline visitor mean that we would encourage its use where tight-coupling is acceptable and type-specific handling is logically localized.

References

[Gamma95] E. Gamma et al., Design Patterns , Addison-Wesley Longman, 1995.

[Krishnamoorthi07] A. S. Krishnamoorthi, ‘The Cooperative Visitor: A Template Technique for Visitor Creation’, 11 July 2007, Artima Developer http://www.artima.com/cppsource/cooperative_visitor.html

[Loki] Loki library http://loki-lib.sourceforge.net/

[Martin] R. C. Martin, ‘Acyclic Visitor’ http://www.objectmentor.com/resources/articles/acv.pdf

[Meyers06] S. Meyers, ‘My Most Important C++ Aha! Moments...Ever’, Artima Developer http://www.artima.com/cppsource/top_cpp_aha_moments.html






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.