An Inline-variant-visitor with C++ Concepts

An Inline-variant-visitor with C++ Concepts

By Jonathan Coe, Andrew Sutton

Overload, 23(129):17-20, October 2015


Concepts are abstract. Jonathan Coe and Andrew Sutton provide us with a concrete example of their use.

Variants allow run-time handling of different types, without inheritance or virtual dispatch, and can be useful when designing loosely coupled systems. J.B.Coe and R.J.Mill [ Mill14 ] previously showed a technique to avoid writing boilerplate code when writing visitors for the traditional inheritance-based Visitor pattern. We modify the inline-visitor technique to handle variant-based visitation and use Concepts [ Sutton13 ] from the Concepts TS [ Concepts ] to switch run-time behaviour depending on syntactic properties of the class encountered.

Variants

A variant is a composite data type formed from the union of other types. At any point the type of the data contained within a variant can only correspond to one of the composite types. Variants are useful when data flow through a program needs to correspond to multiple possible types that are not logically related by inheritance. For instance, the data within a spreadsheet cell could be an integer, a floating point value or a text string. We can represent such a spreadsheet cell by a variant of int , double and string . We can use this variant type within our program as a function argument type or return type and defer type-specific handling to a later point.

The boost libraries offer a variant type [ Boost ] as does the eggs.variant library [ Eggs-1 ]. There are differences between the different offerings of variant but for the purposes of our inline visitor they are much the same and could be implemented as a union of types with an index used to determine which type is active (see Listing 1).

class ExpositionOnlyVariant
{
  union Data_t
  {
    int i_;
    double d_;
    string s_;
  };

  Data_t data_;
  size_t active_type_;
  
  size_t which() const { return active_type_; }
   
  // Other non-illustrative methods and
  // declarations
};
			
Listing 1

Our code examples focus on eggs.variant which is a lot more sophisticated than the exposition-only variant class above (especially where constexpr methods are concerned) [ Eggs-2 ].

Design decisions about exception safety and default construction are interesting and, at the time of writing, under much discussion among the ISO C++ standards body, but beyond the scope of this paper.

A visitor For variants

The Gang of Four [ GoF ] describe the Visitor pattern as “ Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

We can use visitors to implement the type-specific handling our variant may require.

Writing a visitor on a variant type is a straightforward matter that entails determining the value of the discriminator and dispatching to a function that accepts the corresponding concrete type. For example, we could explicitly build a function that applies a visitor for our previously mentioned spreadsheet. See Listing 2.

using Cell = variant<int, double, string>;

template<typename R, typename Visitor> 
R apply(const Cell& c, Visitor&& v)
{
  switch (c.which()) {
    case 0: return v(get<0>(c));
    case 1: return v(get<1>(c));
    case 2: return v(get<2>(c));
    default: throw std::logic_error
      ("Bad variant type");
  }
}

struct DoubleCell_t
{
  int operator()(int n) { return n*2; }
  double operator()(double d) { return d*2; }
  string operator()(string s) { 
    return s.append(s); }
};

Cell DoubleCell(const Cell& c)
{
  return apply<Cell>(c, DoubleCell_t());
}
			
Listing 2

Generic code with templates

Templates in C++ allow the definition of functions and classes with generic types [ Vandevoorde02 ]. We can, for instance, define a generic function that doubles its argument.

  template<typename T>
  T double(const T& t)
  {
    return t*2;
  }
  
  assert(8.0 == double(4.0));

We can define an operator() template for a visitor which will mean it can be invoked on any type. This allows us to specify default behaviour for types that are not explicitly handled (function templates have lower precedence when it comes to overload resolution so a more specific function will be picked where available). See Listing 3.

struct DoubleCellWithGenerics_t {
  template<typename T>
  T operator()(const T& t) { return t*2; }

  string operator()(string s) {
    return s.append(s); }
};

Cell DoubleCellWithGenerics(const Cell& c) {
  return apply<Cell>(c,
  DoubleCellWithGenerics_t());
}
			
Listing 3

This is appealing; we can define default behaviour for our visitor (the generic member function) and define exceptions to this default behaviour (with specific overloads). In this case, the generic version uses operator* for all non- string members of the variant, and the string version resorts to an append member function.

This could become irksome if there were many exceptions, and we have to know about types which need special handling ahead of time. For example, if we later extend Cell to include support for wide-character strings (e.g., std::u16string ), then we would need to add overloads to each visitor function to provide the required functionality. In addition, there's no way we could reasonably expect our visitor to cope with arbitrary user-defined types. Adding date or time representations to Cell would require even more overloads.

However, if we could predetermine sets of abstract data types to which the visitor can be applied, then we could define a small set of generic functions to accommodate those behaviours. That is, some methods would use operator* , while others used append , some might throw exceptions, and so on. Concepts make this easy.

Constraining templates with Concepts

Concepts are an extension to the C++ Programming Language, defined as an ISO Technical Specification [ Concepts ]. The Concepts extension allows the specification of constraints on template arguments in a straightforward, and often minimal way. While, templates can be restricted using SFINAE tricks with enable_if [ Vandevoorde02 ], Concepts provides a more readable notation and vastly improved support for overloading.

Listing 4 is a fully generic visitor written using concepts.

struct DoubleCellWithConcepts_t {
  int operator()(const Numeric& n) { return n*2; }
  string operator()(Appendable s) { 
  return s.append(s); }
};

Cell DoubleCellWithConcepts(const Cell& c) {
  return apply<Cell>(c,
  DoubleCellWithConcepts_t());
}
			
Listing 4

We only need two overloads to fully accommodate the breadth of abstractions included in our Cell representation. The first overload takes an argument of type Numeric and the second an argument of type Appendable . These are concepts. A concept is a compile-time predicate that defines a set of requirements on a template argument.

When a concept name is used in this way, it declares a function that accepts any type that satisfies the requirements of that concept. To help make that more explicit, we could have declared the first overload like this:

  template<typename T>
    requires Numeric<T>()
  int operator()(T n) { return n*2; }

Here, Numeric is explicitly evaluated by a requires clause. Of course, this is more verbose so we prefer notation above.

With this visitor, the Numeric overload defines the behaviour for the int and double members of Cell , For that matter, this will be the behaviour for any numeric type that might include in the future (e.g., vectors or matrices?). Similarly, the Appendable overload defines the behaviour for all sequence-like objects.

In this example, Numeric and Appendable are function templates, declared with the concept specifier. Here is the definition of Appendable that we use above:

  template<typename T>
  concept bool Appendable() {
     return requires(T& t, const T& u) {
       { t.append(u) } -> T&;
     };
  }

The requires expression in the body of the function enumerates the syntactic requirements to be satisfied by a type T . Here, we require T to have a member function append , taking an argument of the same type. The return value, specified by the -> T& must be implicitly convertible to T& . If any of those requirements are not satisfied, the concept is not satisfied.

Concepts are effective ways of specifying syntactic requirements. However, a meaningful concept definition also needs semantic requirements. What is append required to do? The concept used in this example is not especially well-designed because it isn’t obvious how to specify the semantics of an Appendable type, when we have only a single operation. Additional requirements like size and back would be helpful. In the future, we will endeavour to present only complete concepts.

Concepts are a very powerful tool and their first-class status in terms of compiler diagnostics make their consumption by non-expert users much more straightforward. Furthermore, the use of concepts will never add runtime overhead to your program. A technique like type-erasure could be used instead, but it can be difficult to implement, and it adds hidden runtime costs.

The Concepts TS has been sent for publication by ISO and is implemented in GCC trunk [ GCC ].

An inline-variant-visitor with Concepts

Defining the visitor outside the function in which it is used is necessary if it has generic functions. The inline-visitor pattern for the traditional visitor can be adapted to variant visitors and allow definition of concept-constrained generic functions inline at the point of use.

  auto DoubleCell(const Cell& c)
  {
    auto inline_cell_visitor 
       = begin_variant_visitor()
      .on([](Numeric n) { return n*2; })
      .on([](Appendable a) { return a.append(a); })
      .end_visitor();
    return apply<Cell>(c, inline_cell_visitor);
  }

This function is now hardened against future changes in the definition of the variant, Cell . Adding a new Appendable member to that variant would not require changes to this function. In fact, unless a future change adds entirely new categories (i.e., concepts) of members to the variant, we should never have to modify this function again (unless it contains a bug).

The inline-variant-visitor is implemented in much the same way as the inline-visitor [ Mill14 ]: an inner composable function object is recursively built up and finally instantiated.

Conclusion

We have presented a method for generating visitors inline for variants where the run-time type-specific handling can be specified in generic terms depending on syntactic properties of the run-time type. Concept-based handling of variants can facilitate the writing of generic header-only libraries that make use of variant<Ts...> arguments. Type-specific handling of user-defined types, unknown to the library author, can be simply specified in terms of supported concepts.

Although there are slight differences in optimised assembler output with and without the inline-variant-visitor, there is no measurable run-time penalty for its use.

Appendix

Implementation of a concept-enabled inline-variant-visitor (Listing 5).

#include <string>
#include <utility>

// These using declarations are for publication
// brevity only
using std::make_pair;
using std::move;
using std::pair;
using std::nullptr_t;

template <typename F, typename T> concept bool UnaryFunction() {
  return requires(const F &f, const T &t) {
    { f(t) }
  };
}

template <typename F, typename BaseInner,
  typename ArgsT>
struct ComposeVariantVisitor {
  struct Inner : BaseInner {
    Inner(ArgsT &&a) : BaseInner(move(a.second)),
    f_(move(a.first)) {}

    using BaseInner::operator();

    template <typename T>
    requires UnaryFunction<F, T>() 
      auto operator()(const T &t) {
      return f_(t);
    }

  private:
    F f_;
  };

  ComposeVariantVisitor(ArgsT &&args) :
    m_args(move(args)) {}
	
  template <typename Fadd> auto on(Fadd &&f) {
    return ComposeVariantVisitor<Fadd, Inner,
        pair<Fadd, ArgsT>>(
        make_pair(move(f), move(m_args)));
  }

  auto end_visitor() { return Inner(move(m_args)); }

  ArgsT m_args;
};

struct EmptyVariantVisitor {
  struct Inner {
    struct detail_t {};

    Inner(nullptr_t) {}

    void operator()(detail_t &) const {}
  };

  template <typename Fadd> auto on(Fadd &&f) {
    return ComposeVariantVisitor<Fadd, Inner,
        pair<Fadd, nullptr_t>>(
        make_pair(move(f), nullptr));
  }
};

EmptyVariantVisitor begin_variant_visitor() {
   return EmptyVariantVisitor(); }
			
Listing 5

References

[Boost] http://www.boost.org/doc/libs/1_59_0/doc/html/variant.html

[Concepts] Sutton, Andrew (ed). ISO/IEC Technical Specification 19217. Programming Languages – C++ Extensions for Concepts, 2015.

[Eggs-1] http://eggs-cpp.github.io/variant/

[Eggs-2] http://talesofcpp.fusionfenix.com/post-20/eggs.variant---part-ii-the-constexpr-experience

[GCC] svn://gcc.gnu.org/svn/gcc/trunk

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

[Mill14] Robert Mill and Jonathan Coe, ‘Defining Visitors Inline in Modern C++’. Overload 123, October 2014.

[Sutton13] Andrew Sutton, Bjarne Stroustrup, ‘Concepts Lite: Constraining Templates with Predicates’. https://isocpp.org/blog/2013/02/concepts-lite-constraining-templates-with-predicates-andrew-sutton-bjarne-s

[Vandevoorde02] Vandevoorde, David; Nicolai M. Josuttis (2002). C++ Templates: The Complete Guide Addison-Wesley Professional. ISBN 0-201-73484-2.






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.