C++ Language Support for Pattern Matching and Variants

The C++ Programming Language needs a language based variant, or at least P0095R0 argued for it at the 2015 Kona C++ standardization meeting. P0095R0, however, didn’t fully explore generalized pattern matching, which is another desirable feature that is highly related. This post explores some ways to generalize the minimal pattern matching described in P0095R0.

Update 2/25/15. The Jacksonville C++ meeting is only considering proposals for C++17. Given that this is a C++20 paper, we will publish the next revision at the Oulu meeting in June.

Preliminaries

We assume the reader has a basic understanding of both variants and pattern matching as language features. A quick skim of P0095R0 should help in this respect. The intent with this post is to gather some early feedback before presenting the next revision of P0095 at the 2016 Jacksonville C++ committee meeting.

Note that the features described here are not being proposed for C++17, but for the following standard revision.

Review

This section reviews language-based variants.

It’s difficult to imagine going without variants once you have them. We’ve had library-based variants for a while thanks to Boost.Variant and an improved variant library is likely coming with C++17. That’s great news for the C++ community. On the other hand, variant libraries can be difficult to use in practice when it comes to recursion, forward declarations, identifier-based inspection, and creating new types. That’s where variants as a language feature becomes a compelling alternative.

Language based variants (what we’re calling lvariant for now 1) are a lot like simple structs. They both have names and a list of fields with types and identifiers. Consider the following example:

C++

1

2

3

4

5

lvariantuser_information{

std::stringname;

intid;

};

There is a critical distinction in semantics, however. Instances of type user_information, as declared above, consist of either a nameor an id. If user_information were defined to be a struct, its values would consist of both a nameand an id. When you think of a variant’s members, think either or. When you think of a simple struct’s members, think both and.

The user_informationlvariant above could represent a form entry which contains either a user name or a numeric user ID.

It turns out that variants come up frequently in every day programming. Imagine you are writing a data structure representing a git command-line abstract syntax tree (AST) where certain flags are available only when the corresponding command is active. An lvariant can represent the top of the AST.

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// The flag state for the "init" command.

structgit_init_flags{

boolquiet;

boolbare;

// etc.

};

// The flag state for the "clone" command.

structgit_clone_flags{

boolmirror;

std::stringrepository;

std::optional<std::string>directory;

// etc.

};

// Indicates the active command and the associated flags.

lvariantgit_command_line_parse{

git_init_flags init;

git_clone_flags clone;

// etc.

};

Instances of git_command_line_parse will either have git init flags (init) or git clone flags (clone).

Lets look at another example. JavaScript Object Notation (JSON) is a common interchange format for web frameworks. An lvariant can implement an in-memory representation of a JSON document:

C++

1

2

3

4

5

6

7

8

9

lvariantjson_value{

std::map<std::string,std::unique_ptr<json_value>>object;

std::vector<std::unique_ptr<json_value>>array;

std::stringstring;

doublenumber;

boolboolean;

std::monostate null;

};

There are a couple important things to point out in this example.

We achieve recursion using std::unique_ptr. Like structs, lvariant fields can make use of the lvariant‘s name to achieve recursion. Also like structs, the type is “incomplete” and must be wrapped in a pointer of some sort.

std::monostate is a type included in the current language-based variant proposal. In both library and language based variants, std::monostate is used for fields which don’t carry any extra data. Its definition is simply struct monostate {};.

Before we move on, lets define some useful terms for discussing pattern matching and variants in C++. We use the word “piece” to denote a field in a struct. The word “alternative” is used for lvariant fields. The programming language theory savvy will also recognize lvariants to be sum types and simple structs to be product types, although we won’t use that jargon here.

Pattern matching integrals and enums

The most basic pattern matching is that of integral (ie. int, long, char, etc.) and enum types, and that is the subject of this section. Before we get there, however, we need to distinguish between the two places pattern matching can occur. The first is in the statement context. This context is most useful when the intent of the pattern is to produce some kind of action. The if statement, for example, is used in this way. The second place pattern matching can occur is is in an expression context. Here the intent of the pattern is to produce a value of some sort. The trinary operator ?:, for example, is used in this context. Upcoming examples will help clarify the distinction.

Each context uses a different pattern matching keyword. inspect_s is used in statement contexts. inspect_e is used in expression contexts 2.

In the following example, we’re using inspect_s to check for certain values of an int i:

C++

1

2

3

4

5

6

7

8

9

10

inspect_s(i){

0=>

std::cout<<"I can't say I'm positive or negative on this syntax."

<<std::endl;

6=>

std::cout<<"Perfect!"<<std::endl;

_=>

std::cout<<"I don't know what to do with this."<<std::endl;

}

The _ character is the pattern which always succeeds. It represents a wildcard or fallthrough. The above code is equivalent to the following switch statement.

C++

1

2

3

4

5

6

7

8

9

10

11

12

switch(i){

case0:

std::cout<<"I can't say I'm positive or negative on this syntax."

<<std::endl;

break;

case6:

std::cout<<"Perfect!"<<std::endl;

break;

default:

std::cout<<"I don't know what to do with this."<<std::endl;

}

inspect_e can be used to pattern match within expression contexts as in the following example. c is an instance of the colorenum:

C++

1

2

3

4

5

6

7

8

9

10

11

enumcolor{red,yellow,green,blue};

// elsewhere...

constVec3 opengl_color=inspect_e(c){

red=>Vec3(1.0,0.0,0.0)

yellow=>Vec3(1.0,1.0,0.0)

green=>Vec3(0.0,1.0,0.0)

blue=>Vec3(0.0,0.0,1.0)

};

All we’ve seen so far is a condensed and safer switch syntax which can also be used in expressions. Pattern matching’s real power comes when we use more complex patterns though. We’ll see some of that later.

Pattern matching structs

Pattern matching structs isn’t all that interesting in isolation: they merely bind new identifiers to each of the fields.

C++

1

2

3

4

5

6

structplayer{

std::stringname;

inthitpoints;

intcoins;

};

C++

1

2

3

4

5

6

7

voidlog_player(constplayer&p){

inspect_s(p){

{n,h,c}

=>std::cout<<n<<" has "<<h<<" hitpoints and "<<c<<" coins.";

}

}

struct patterns aren’t limited to binding new identifiers though. We can instead use a nested pattern as in the following example.

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

voidget_hint(constplayer&p){

inspect_s(p){

{_,1,_}=>std::cout<<"You're almost destroyed. Give up!"<<std::endl;

{_,10,10}=>std::cout<<"I need the hints from you!"<<std::endl;

{_,_,10}=>std::cout<<"Get more hitpoints!"<<std::endl;

{_,10,_}=>std::cout<<"Get more ammo!"<<std::endl;

{n,_,_}=>if(n!="The Bruce Dickenson")

std::cout<<"Get more hitpoints and ammo!"<<std::endl;

else

std::cout<<"More cowbell!"<<std::endl;

}

}

While the above code is certainly condensed it also lacks some clarity. It is tedious to remember the ordering of a struct‘s fields. Not all is lost, though; Alternatively we can match using field names.

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

voidget_hint(constplayer&p){

inspect_s(p){

{hitpoints:1}

=>std::cout<<"You're almost destroyed. Give up!"<<std::endl;

{hitpoints:10,coins:10}

=>std::cout<<"I need the hints from you!"<<std::endl;

{coins:10}

=>std::cout<<"Get more hitpoints!"<<std::endl;

{hitpoints:10}

=>std::cout<<"Get more ammo!"<<std::endl;

{name:n}

=>if(n!="The Bruce Dickenson")

std::cout<<"Get more hitpoints and ammo!"<<std::endl;

else

std::cout<<"More cowbell!"<<std::endl;

}

}

Pattern matching lvariants

Pattern matching is the easiest way to work with lvariants. Consider the following binary tree with int leaves.

C++

1

2

3

4

5

lvarianttree{

intleaf;

std::pair<std::unique_ptr<tree>,std::unique_ptr<tree>>branch;

}

Say we need to write a function which returns the sum of a tree object’s leaf values. Variant patterns are just what we need. A pattern which matches an alternative consists of the alternative’s name followed by a pattern for its associated value.

C++

1

2

3

4

5

6

7

intsum_of_leaves(consttree&t){

returninspect_e(t){

leafi=>i

branchb=>sum_of_leaves(*b.first)+sum_of_leaves(*b.second)

};

}

Assuming we can pattern match on the std::pair type, which we’ll cover later, this could be rewritten as follows.

C++

1

2

3

4

5

6

7

intsum_of_leaves(consttree&t){

returninspect_e(t){

leafi=>i

branch{left,right}=>sum_of_leaves(*left)+sum_of_leaves(*right)

};

}

More complex datatypes

Pattern matching can make difficult code more readable and maintainable. This is especially true with complex patterns. Consider the following arithmetic expression datatype:

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// An lvariant (forward) declaration.

lvariant expression;

structsum_expression{

std::unique_ptr<expression>left_hand_side;

std::unique_ptr<expression>right_hand_side;

};

lvariantexpression{

sum_expression sum;

intliteral;

std::stringvar;

};

We’d like to write a function which simplifies expressions by exploiting exp + 0 = 0 and 0 + exp = 0 identities. Here is how that function can be written with pattern matching.

C++

1

2

3

4

5

6

7

8

9

// The behavior is undefined unless `exp` has no null pointers.

expression simplify(constexpression&exp){

returninspect_e(exp){

sum{*(literal0),rhs}=>simplify(rhs)

sum{lhs,*(literal0)}=>simplify(lhs)

_=>exp

};

}

Here we’ve introduced a new * keyword into our patterns. *(<pattern>) matches against types which have a valid dereferencing operator and uses <pattern> on the value pointed to (as opposed to matching on the pointer itself). A special dereferencing pattern syntax may seem strange for folks coming from a functional language. However, when we take into account that C++ uses pointers for all recursive structures it makes a lot of sense. Without it, the above pattern would be much more complicated.

Pattern matching tuple-like types

Now we have patterns for integrals, enums, simple structs, and lvariants. Is there a way to enable pattern matching for custom data types? The answer, of course, is yes.

Tuple-like types are those which behave a lot like simple structs. These objects represent a sequence of values of various types. std::pair and std::tuple are notable examples. In this section we’ll see how we can annotate custom tuple types for pattern matching.

Pattern matching for tuple-like types is accomplished by overloading the extract operator. Imagine we have a custom pair type that has its m_first and m_second member variables declared private. We overload the extract operator as follows:

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

template<classT1,classT2>

classpair{

T1 m_first;

T2 m_second;

public:

// etc.

operatorextract(std::tuple_piece<T1>x,std::tuple_piece<T2>y){

x.set(&this->first);

y.set(&this->second);

}

};

The signature of the extract operator function provides both the number of pieces and the type of each piece. The code in the body of this operator overload connects the actual pieces m_first and m_second to their placeholders x and y. This is all the information required for the compiler to use tuple-like objects in pattern matching.

C++

1

2

3

4

5

inspect_s(std::make_pair(3,std::string("Hello World")){

{3,s}=>std::cout<<"Three, a special number, says "<<s<<std::endl;

{i,s}=>std::cout<<i<<" a boring number, says "<<s<<std::endl;

}

Pattern matching variant-like types

We would also like to generalize matching for variant-like types. Our example is an either template. It is the variant analogue to std::pair.

C++

1

2

3

4

5

6

template<classT1,classT2>

lvarianteither{

T1 left;

T2 right;

};

Of course, the above implementation will pattern match without modification since we are using an lvariant. Let us consider, for the sake of discussion, that the data type was implemented as follows.

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

template<typenameT,typenameU>

classeither

{

public:

enumselection{left,right};

private:

selection m_selection;

Tm_left;

Um_right;

public:

either():m_selection(left){}

either(Tt):m_selection(left),m_left(t){}

either(Uu):m_selection(right),m_right(u){}

boolselection()const{returnm_selection;}

T&get_left(){

assert(m_selection==left);

returnm_left;

}

U&get_right(){

assert(m_selection==right);

returnm_right;

}

either<T,U>&operator=(consteither<T,U>&other){

m_selection=other.m_selection;

m_left=other.m_left;

m_right=other.m_right;

}

either<T,U>&operator=(constT&t){

m_selection=left;

m_left=t;

}

either<T,U>&operator=(constU&u){

m_selection=right;

m_right=u;

}

};

To enable pattern matching for this type, we need to implement two operator overloads: discriminator and alternative.

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

template<typenameT,typenameU>

classeither{

//...

public:

//...

selection operatordiscriminator(){

returnm_selection;

}

operatoralternative(std::variant_piece<T,left>x){

x.set(&m_left);

}

operatoralternative(std::variant_piece<U,right>x){

x.set(&m_right);

}

};

The discriminator operator overload returns a integral or enum value corresponding to the question of which alternative is currently active.

The alternative operator is an overloaded function taking in a single std::variant_piece parameter. The first template argument of std::variant_piece is the type of that alternative. The second template argument is a value of the return type of discriminator3.

Now we have enough information to use our specialized either class in pattern matching:

C++

1

2

3

4

5

6

7

8

9

either<std::string,int>e=/* etc. */;

inspect_s(e){

left error_string

=>std::cout<<"You've got an error: "<<error_string<<std::endl;

righti

=>std::cout<<"You've got the answer "<<i<<std::endl;

}

Conclusions

Hopefully this post gives you a good idea of the direction we’re headed in with regard to generalized pattern matching and language-based variants. There are still topics to be fleshed out such as the desirability of pattern guards and whether match <pattern>: syntax is preferable to <pattern> =>.

Any thoughts on what’s been presented? Any good ideas for improvement? Constructive comments are very welcome.

Acknowledgements

Thanks to Vincente Botet Escriba, Bjarne Stroustrup, Bengt Gustafsson, and the C++ committee as a whole for productive design discussions. Also, Yuriy Solodkyy, Gabriel Dos Reis, and Bjarne Stroustrup’s prior research into generalized pattern matching as a C++ library has been very helpful.

P0095R0 originally used the syntax enum union for a language based variant. We’re using lvariant here as a placeholder as the committee thought the enum union syntax could create unnecessary confusion. ↩

P0095R0 originally used switch and case syntax for pattern matching. They’ve been changed here to eliminate confusion as suggested by the committee. ↩

This syntax will only work if P0127R0 goes into the language, which seems likely. Otherwise, we would need to explicitly specify the discriminator type as in std::variant_piece<T, selection, left>. ↩

If I understand it correctly, Scala’s unapply approach used on a custom variant type would make pattern matching consist of several if/else if clauses. As a result, it would be O(n) where n is the number of alternatives in the variant.

The approach presented in the post allows one to keep O(1) matching for custom variant types.

Pattern matching in general form is intrinsically sequential. In some case you can do optimisations but it is hard. So is exhaustion analysis.
[There was a recent paper on how to do this, but it is impossible with guards anyhow]

If you want to see how to do fully generalised pattern matching look at Felix, which has user defined patterns. Felix generates C++ (so I personally have been able to do variants in C++ for ages 🙂

This requires TWO functions: a disciminator which is PREDICATE. Yes it matches, No it doesn’t.

The second function is the extractor, which get the argument from the value. Only required if there is an argument.

Patterns in reasonable generality can have predicates as guards. Since the meaning of the guard cannot be determined, guarded match branches MUST be handled sequentially.

I agree that general pattern matching with guards is intrinsically sequential. That being said, I feel like allowing the compiler to optimize for the common case where it it isn’t sequential is very important for systems language like C++.

Even when some guards are used in a particular inspect statement, the compiler should have the ability to make O(1) jumps where it can. Discriminators as predicates prevents this.

Can you substantiate your claim that if/else chains are faster than indexed jumps with some performance benchmarks?

Also you claim that first class tuples are a prerequisite to variants and pattern matching. Can you elaborate on that? It isn’t immediately obvious to me.

@David, no i cannot substantiate my claim if/then/else chains are faster. It’s likely to be architecture dependent. Most modern Intel CPU’s have pipelining and speculative branching which would be defeated by a computed jump. Obviously there’s a complex tradeoff related to the number of cases in the branch.

My point would be, do not design the pattern matching semantics so they’re restricted by this requirement. IMHO it’s much more important to get good general semantics into a language. Do not forget that a compiler can *calculate* in some cases when a computed jump can replace an if/then/else chain: I would guess clang can do that (but I’m not sure).

It’s possible given an if/then/else chain that more benefit would be derived from lifting the conditionals out of the code so they’re all in one place with an extra jump to the match handler. One extra jump to code not in the cache, to ensure the sequence of tests are done by instructions which *are* all cached. Again, architecture dependent.

The thing is a lot of pattern matches cannot use a jump on the outside cases anyhow. For example (in Felix/ML notation sorry)

match x with | 1 => .. | 3 => .. | 27 => ..

which is just a C switch. There’s no choice. You have to use an if then else chain.

When the branch alternative is used the unique_ptr can not be nullptr. I believe that a more constraining value type class must be used instead, something like boost::recursive_wrapper. recursive_wrapper is a value type class that should be implicitly convertible to tree&.

> I believe that a more constraining value type class must be used instead, something like boost::recursive_wrapper.

I don’t think that introducing another concept here is necessary as it is with a library-based variant. The ability to denote a not-null pointer in C++ is an orthogonal problem that other proposals are attempting to solve.

The ‘not_null’ type in the C++ Core Guidelines would solve this cleanly, for example.

IMO, when an efficient library solution exists it is worth using it instead of adding new features to the language except if the library solution is not friendly. Do you consider the tuple-like access not friendly as customization point?

After some thought, using specific operators would allow to define the language feature independently of the library.

However I don’t see how the extract operator could take in account the following case. If we want to extract 3 elements from int v[3] as PO144 does, this should be predefined as we can not define the extract function.

After some thought, using specific operators would allow to define the language feature independently of the library.

Have you think on having two operators for product types, one to obtain the number of elements and the other to obtain the nth element? This could help to have a more efficient implementation when the pattern ignores a specific element or when the pattern use specific names.

I don’t see how having two operators for product types is going to have any effect on either compile-time or run-time efficiency. The compiler can inspect the number of arguments for that function without issue IIUC.

> I would like to see inspect to be variadic so that matching multiple arguments is possible.

What do you see as the benefits of adding this feature when compared with using, say, std::make_tuple?

> Structured binding should be a particular case of pattern matching.

I agree that this would be nice.

> This would mean that the user must be able to say if the capture of variables of the pattern is done by copy or by reference. How could the user do that with your ongoing proposal?

Good question. I haven’t given it much thought until now. I suppose there isn’t much difficulty in allowing type qualifications before identifiers in patterns. Structured binding would default to value types where binding within inspect would default to either && or const & depending on the const-ness of the value being matched.

This brings up another question. What do we do when a pattern doesn’t match with structured binding? For example:

If the match could fail, you add a wildcard branch throwing an exception. If the user specifies it with a compiler flag, you can issue a warning.

If a branch exists which cannot be taken, it is not a good idea to always issue a warning. Felix compiler actually generates such matches and the user would get confused being warned about generated construct. Also programmers may add a premature wildcard deliberately during development or debugging.

In an if/then/else chain, a terminal wildcard throwing an exception does not cost any time (only code space). And you have to do *something*. Felix aborts the program. Ocaml throws. C++ should throw IMHO.

For what it’s worth all sensible libraries (including Google Mock) put their operator _ in a namespace: In GMock’s case it’s testing::_ (though most people put using namespace testing;). So i don’t see it as being a practical problem.

lvariant is different from union in that values of an lvariant encode which field is currently active whereas values of unions do not encode that information.

The committee rejected the name “enum union”, but not the concept. “enum union” was thought to create too much confusion since “enum” and “union” are already well-understood keywords.

The “case” syntax was rejected for similar reasons. In this case there was a concern that new people would see ‘case’ being used where no ‘break’ was necessary and forget to use ‘break’ when it actually is necessary.

I should clarify my comment, and either further expose my ignorance or make explaining the background already tread easy. Why not extend union such that if no tag is manually specified in the members, the member name becomes the tag? If there are shared attributes of the same type and name, that is assumed the tag, and union is treated as legacy?

lvariants use the member name as the tag as you suggest. Folks still have a need for plain unions though. Making unions act like lvariants would get a lot of resistance in the committee since it would change the meaning of old code. For example, the size of unions would increase.

Re real tuples: if you do not have real tuples known to the compiler, you have a blob type. The compiler than is unable to extract components with projections. How would you match a pair? You have to match with the two patterns:

match e with px,py => …

For the branch to be taken, px must match the first component of e, and py the second component. How can you even perform the test if e is just a blob type? (Abstract type). The problem is that you have used structural typing in the match, and the argument must be structurally typed as well.

User defined matching can solve this. But that’s what I recommended: you must have separate functions to (a) check if the pattern matches and (b) extract the pieces if it does. Then the construction is supposed for any type in the library that cares to define how it can be pattern matched.

Consider:

match (a,b) with
| 1,v1 => ..
| 1,v2 =>
| 1,v3 => …

where v1/v2/v3 are cases of a variant, to optimise this you would do a if/then/else on the first component, then in the true branch equal 1, you can do a computed jump on the variant index for the second branch. You can’t do that unless the type of the tuple second component is a known variant type and that means you also need to know the type of the argument. You have to know its a tuple and what a tuple is: tuples have to be built in to the language.

Vastly .. and I mean VASTLY in big fat capital letters .. more important than worrying about optimisation is being able to do compile time exhaustion analysis. This is actually quite hard but algorithms exist in the academic literature.

Introduce lvariants and low level inspect. These are simple and easy and you can use a computed jump for performance if it is faster than conditional chain. However it isn’t general pattern matching and can’t easily be extended to it because it allows only a single jump-on-tag operator plus a single extract argument operator for each variant constructor (which is probably universal plus a cast).

Or you can introduce general pattern matching with two operators per branch: one to check if the pattern is matched and one to extract the required pieces. This generalises to ANY data type with user defined checkers and extractors.

The first extension may be more likely to pass the committee, which probably doesn’t have much understanding of structural typing and algebraic data types and the wealth of theoretical research into it. However the more general proposal makes the extension significant enough to be seriously worthwhile because it can be retrofitted to ANY data type including tuples in the library, any user defined structs, and even classes with virtual dispatch stuff.

Pattern matching in general form is intended to make programming easier. At the core the lvariant decoding is the real functionality, but people using languages with general pattern matching use it heavily. In fact I would say over 50% of all code in most programs is pattern matching. One must remember that “if/then/else” is nothing but a special case of a pattern match, and bool is nothing but a 2 case lvariant with no arguments to the constructors true and false. In fact in Felix this is literally the case: all conditionals are desugared to pattern matches (the code generator converts all pattern matches back to if then else chains .. because it is generating C++ which doesn’t have pattern matching 🙂

I’m not on the committee more but I would guess you will have hard time with either proposal. I proposed variants myself for the 89 Standard, but at that point few had any idea what a sum type was. Given that Boost variant is completely wrong, all I can say is: Good luck! lvariants are the theoretically correct machinery.

1. map pattern matches to if then else chains, forget about computed jumps.

2. implement an optimisation in the compiler that recognises if then else chains which decode a compact subrange of integer values and optimise that to a range check followed by a computed jump.

The range check can be elided if the type is recognised as a subrange of int, even if the user cannot define such types the variants can use them for the variant tags.

The advantage of decoupling these things is that the optimisation can apply to ANY code not just inspects. It will automatically consider the one case C++ already understands: booleans (actually one might consider char as well, 256 case jumps may be reasonable on modern computers).

In addition, it can be used to re-encode sum types. For example consider:

if x then
if y then .. else ..
else
if y then ..
else ..

But this can be done in a SINGLE computed jump on value x + 2 * y. And there is no “inspect” statement there: the optimisation is more general, it doesn’t care how the branches are created. It works for generated ones and cases where the user wrote the code out by hand.

David is a software architect/C++ expert working at Bloomberg in New York City.