
Type-Safe Unions in C++ and Rust - Tatyanazaxarova
http://genbattle.bitbucket.org/blog/2016/10/07/Type-Safe-Unions-in-C-and-Rust/
======
flyx86
> but it’s the first language I’ve experimented with that has made them a
> first-class feature

It is a feature of ALGOL68, Pascal, Ada and quite some newer languages:

[https://en.wikipedia.org/wiki/Tagged_union](https://en.wikipedia.org/wiki/Tagged_union)

~~~
Manishearth
One interesting thing about Rust is that none of the language features are
really _new_. Even the borrow checker is from research papers and languages
from quite a while ago. The only thing I can think of that might be truly
unique to Rust is the concurrency safety model (Send+Sync), though that might
be old too.

Rust has just managed to take all these features and put them together well,
and strive to be more than a research language by working on things that would
make others actually _use_ the language.

(Of course, this _particular_ feature is common in many, many languages)

~~~
bandrami
_the concurrency safety model (Send+Sync), though that might be old too_

Smalltalk has had that since the early 1980s

~~~
dbaupp
Like steveklabnik, I'm extremely curious to hear more about how Smalltalk's
concurrency model is just like Rust's. The latter is a fairly flexible model
to defend against data races (which seems to be a concept only really
formalised in the late 80s) that puts most of the power in the programmer's
hands (i.e. no need for compiler-inserted locks on every object, etc.) that
comes from a finely balanced combination of the trait system and the manner in
which Rust controls mutation.

It would be great to see how other languages have achieved a similar balance,
and my impression is that Smalltalk (and like most languages) does not put
nearly as much effort into controlling mutability, but maybe I'm wrong. Could
you post some links/a description that explores how Smalltalk achieves a
similar level of safety?

~~~
nickpsecurity
"which seems to be a concept only really formalised in the late 80s"

[http://brinch-hansen.net/papers/1975a.pdf](http://brinch-
hansen.net/papers/1975a.pdf)

[http://brinch-hansen.net/papers/](http://brinch-hansen.net/papers/)

His first, concurrent OS was RC 4000 in 1969. It had many mechanisms in place.
He got the key parts of the safety problem figured out by 1972. His language
to handle much of it statically at compile-time was done in 1975. His Boss 2
system same year used coroutines + similar concepts to run 100 activities at a
time with a proof of deadlock freedom. Finally, he used Concurrent Pascal to
implement Solo system which ran its processes safely without using physical,
memory protection. These summaries came from "Evolution of Operating Systems"
on 2nd link.

So, the stuff was well-established by the mid-70's with operating systems
using it in production. Just ignored by mainstream like a lot of good stuff
for various reasons. ;)

~~~
dbaupp
I don't have time to read it in full right now, but a quick glance over that
paper doesn't show any formalisation of data races, which is specifically what
that parenthetical is about, not just general "concurrency safety". Having a
detailed description of concept seems important because Rust is fairly precise
in what it defends against, possibly giving the programmer more flexibility
and power than in a system that attempts to solve many problems (but of course
giving them less assistance when writing something that fits within the rules
of the more defensive languages). Of course, it's possible that a language
"accidentally" solves that problem without it having been formalised, it just
seems less likely.

~~~
nickpsecurity
It's in the disk buffer and monitor examples. Here's a relevant quote:

"A disk buÆer is a data structure shared by two concurrent processes. The
details of how such a buÆer is constructed are irrelevant to its users. All
the processes need to know is that they can send and receive data through it.
If they try to operate on the buÆer in any other way it is probably either a
programming mistake or an example of tricky programming. In both cases, one
would like a compiler to detect such misuse of a shared data structure."

"To make this possible, we must introduce a language construct that will
enable a programmer to tell a compiler how a shared data structure can be used
by processes. This kind of system component is called a monitor. A monitor can
synchronize concurrent processes and transmit data between them. It can also
control the order in which competing processes use shared, physical
resources."

Sounds like he understands the problem is concurrent processes stepping on
each others toes using a shared, data structure. He has nice drawings and
charts to go with it. Plus, an early model to solve it statically at compile
time. Jweb_Guru noted some limitations of his method but it understands and
solves the fundamental problem. Hansen's own work improved on it later plus it
was obsoleted by things like Ravenscar, SCOOP and now Rust's method. Nice that
he had a whole OS protected from concurrency errors at compile time in the
70's, though. :)

~~~
dbaupp
None of that looks like a formalisation or even a description of a data race,
at least not in the modern sense.

~~~
nickpsecurity
Im not a concurrency expert. Just had basic explanations and training common
with other developers. How it was explained to me was two or more tasks trying
to simultaneously access a shared resource for reading or writing. These
accesses might not happen in the desired order, causing incorrectness. Then
there were lock-related issues on top of that.

Hansens work formalized what I just described in terms of English, diagrams,
and compiler checks. He started with sequential operations on private data in
modules. He says if two or more share thd same private data they might not
execute in desired order. The monitor pattern enforced user-specified order on
function calls to shared data. Built-in to language & compiler.

If my description of race conditions is inaccurate or insufficient, I'd
appreciate a link to one that you think is more accurate that I could use as a
comparison point against the Hansen paper. Otherwise, his description of
problems implementing concurrency sounds exactly like what I learned in books
on multithreading, supercomputing, etc. Shared resource used in incorrect
order due to concurrency.

Note: Also, his colleagues Dijkstra and Hoare were still inventing and
developing formal verification at the time. Tooling sucked. Standard practice,
like he did with Algol and COBOL compilers, was writing things like this in
precise English with code examples or diagrams. Not sure if you were expecting
a HOL model or something when you said "modern" but I figured Id mention stuff
was primitive then.

~~~
dbaupp
I'm not looking for a formal mathematical model or anything, just a precise
plain English description of a data race ( _not_ a race condition), e.g. like
the following:

A data race is when

\- two threads access a single memory location,

\- at least one of which is a write, and

\- at least one of which has no synchronisation^

(^ synchronisation in the sense of things like atomic instructions, not
necessarily full locks.)

~~~
nickpsecurity
Alright that's clear. It's also in the first paper I gave you in the first
illustration. The problem you're having is you've constrained the definition
of concurrency or race conditions to only be about the language common among
application programmers, esp threading. Concurrency is broader than that: it
can apply to processes, threads, systems, protocols, or even hardware
circuits. All that is required is two or more active things working with a
shared resource in a way where operations might get interleaved and out of
order in a way that makes computation incorrect.

Knowing that, Hansen starts on problem with RC4000 (1969) describing
protecting communication between "processes" via message buffers, message
queues, and checks performed on them to ensure validity. That and Dijkstra's
critical regions are where the foundations were laid. His next step was the
formalization the problem in 1972:

[http://brinch-hansen.net/papers/1972a.pdf](http://brinch-
hansen.net/papers/1972a.pdf)

Section's 1 and 2 cover basics of concurrent operation + race conditions. It
was clear to me by the abstract but should be extra clear he's talking about
them by this statement about Algorithm 1:

"The copying, output, and input of a record can now be executed concurrently.
To simplify the argument, we will only consider cases in which these processes
are _arbitrarily interleaved_ but not overlapped in time. The erroneous
concurrent statement can then be _executed in six different ways_ with three
possible results."

That's definitely a concurrency error. He then talks about mutual exclusion
and synchronization with an await primitive. Closer to modern language but the
focus on operating systems _in the early 1970 's_ means he says processes
instead of threads and things like disk buffers or message queues instead of
shared memory. Although his examples in 1972 paper are clearly shared memory
since it's at algorithm level.

So, they discovered the problem around late 60's, implemented an early
solution for sharing resources among processes in 1969, fully described race
conditions + some other stuff by 1972, had a safer-by-design language to catch
it at compile time by 1975, and applied that to implement a concurrent,
production OS (Solo) for day-to-day use by academics by 1976.

So, race conditions were both formalized and initially solved in early to mid
1970's. I don't know how much more you need given they had an OS running 100
jobs/users at once interacting on shared resources without visible failures
using their concurrency model. Mainstream OS's and apps are still having race
conditions pop up on occasion. Clearly, what they were doing was addressing
root cause if no race conditions occurred after a successful compile of
"multiprogrammed" apps. :)

~~~
dbaupp
You're still talking about "race conditions" and "concurrency errors", whereas
I am talking specifically about data races _not arbitrary race conditions or
concurrency errors_. I don't see how the first illustration demonstrates
anything about data races, nor how the rest of your comment applies to this: a
data race isn't just plain old interleaving of executions/possible results.

A data race is when a program may give undefined results (in the sense of
undefined behaviour in C) because you've violated core language semantics.
Solving "all" concurrency problems is a much broader and far more restrictive
paradigm than just ensuring that a program has defined behaviour, and not
having restrictions is key to systems programming. (In fact, it's not even
clear to me how one can "solve" race conditions at a language level without
tying into some sort of machine-readable product specification or without just
removing concurrency entirely: a single piece of non-determinism may be fine
in one program but bad, i.e. a race condition, in another.)

 _> The problem you're having is you've constrained the definition of
concurrency or race conditions to only be about the language common among
application programmers, esp threading_

No, I'm not having any problem here: you said yourself that you're not an
expert. A data race ( _not arbitrary race condition_ ) only makes sense if
there's shared memory and thus thread is a perfectly reasonable description
(although "thread" is also often used to just mean thread of execution, not
literal pthread thread). It's becoming clear that we're talking about
different things since you're not in-tune with the jargon/subtle terminology.

~~~
nickpsecurity
That is in fact where dispute came from: race conditions vs data races. I saw
race and concurrency thinking you were talking about race conditions. My
mistake.

Hmm. Ill have to look into the old stuff further to see about whether any of
it, including Hansen, covers the accepted definition of data races. There's
potential that Hansen's does but Im holding off until I think on it more.

------
logicchains
Note C++2017's std::variant is based upon boost::variant, which has been
around since at least 2004
([http://www.boost.org/doc/libs/1_31_0/doc/html/variant.html](http://www.boost.org/doc/libs/1_31_0/doc/html/variant.html),
[http://www.boost.org/users/history/](http://www.boost.org/users/history/)).

boost::variant however lacks a nice visit method that takes lambdas, instead
requiring the user to create visitor classes. This verbosity may be part of
the reason it wasn't adopted in mass, in spite of its advantages in terms of
type safety.

~~~
hellofunk
They are implemented completely differently. The only real similarity is in
the name and API. Boost variant suffers from significant performance penalties
that the new std::variant does not have.

~~~
gpderetta
Boost.variant suffers for being correct by default (i.e. strongly exception
safe) plus opt-in for speed, while the new std::variant can get in an invalid
state if an exception is thrown at a bad time. Let's say that the trade-offs
will be hotly debated until the standard actually ships.

~~~
Sharlin
The tradeoffs have been hotly debated for years :) The current spec is finally
something the committee could agree on, after countless proposals, counter-
proposals, endless email discussions and long evenings at committee meetings.

~~~
gpderetta
I agree that the current spec is better than none at all, but it feels like a
bad compromise. I would have preferred either a fully exception safe variant
or one with an explicit empty state.

------
verroq
Isn't this just an algebraic data structure?

~~~
gpderetta
well, yes. The nice thing is that in C++ can be implemented purely as a
library.

Sometimes C++ feels like the high level languages assembler.

~~~
bjz_
Unfortunately that means they lack lots of the pattern matching niceties that
you get in languages with builtin ADTs. There is an impressive paper about
implementing pattern matching on sub-classes, but it's pretty hackily done
using the preprocessor, and could definitely do with some language support:
[http://www.stroustrup.com/OpenPatternMatching.pdf](http://www.stroustrup.com/OpenPatternMatching.pdf)

~~~
hellofunk
It is widely speculated that pattern matching and many other syntactic
enhancements are coming to C++ because of the big door that std::variant has
opened.

~~~
squidbidness
IIRC Stroustrup mentions a desire for features such as these for versions of
C++ after C++17.

~~~
MichaelMoser123
C++11 c++14 c++17 does that mean that we might see a match statement in c++20?
It would be a significant addition - similar in complexity to anonymous
functions.

~~~
MichaelMoser123
i remember that Bjarne Stroustrup held a talk in cppcon15 where he talked a
lot about the GSL library [1] - it would add type annotations that can be
checked by a tool, so as to check for potential memory problems (to me that
sounds like a poor man's borrow checker).

One year later: i see the template library [1] but i don't see the analysis
tool. Does anybody know what happened with this initiative?

[1]
[https://www.youtube.com/watch?v=1OEu9C51K2A](https://www.youtube.com/watch?v=1OEu9C51K2A)

[2] [https://github.com/Microsoft/GSL](https://github.com/Microsoft/GSL)

~~~
pjmlp
The tools are called clang tidy and Visual Studio 2015 Update 3.

Other vendors might eventually add support as well.

------
Benjamin_Dobell
Believe it or not, even a weakly typed language like JavaScript can take
advantage of type-safe unions, thanks to Flow -
[https://flowtype.org/docs/union-intersection-
types.html](https://flowtype.org/docs/union-intersection-types.html)

------
jjnoakes
Is there any demand for a syntactic sugar layer on top of C++? Something to
make these new features more ergonomic? Something one could opt-in to for
newer code, or code that doesn't need to be backward compatible to 1990?
Something that outputs valid C++ and so works with any tool chain?

~~~
pcwalton
That would be a compiler for a language to C/C++. That design decision is
virtually always a bad idea: LLVM has simpler semantics, allows proper GC,
allows proper debug info, is widely portable, and avoids a needless AST
serialization/deserialization step.

Don't compile to C.

~~~
jjnoakes
Please don't tell others what not to do.

First, there are cases where compiling other languages to C or C++ is better
than via LLVM; interoperability and portability are two (yes, C and C++ are
more portable than LLVM).

Second, I'm not really talking about compiling some wildly different language
to C++. I'm talking about some simple syntactic sugar. The same syntactic
sugar that would be in an existing C or C++ compiler's front end, except
instead of adding it to all the frontends in the world and waiting years, I'd
like to implement it just once and now.

~~~
pcwalton
Effort in this direction would be better spent just adding new features to
clang (or GCC). It'll be so much easier than writing a new parser and semantic
analysis for C++, which you will have to do if you want the new feature to
behave properly in the presence of templates, etc. You'll end up wanting to
just use clang's front end to help you, like most C++ tools do—and at that
point why not just modify clang itself?

People bring up obscure platforms (though, interestingly, I can't recall
anybody actually _naming_ one of those platforms) all the time as motivation
for compiling to C. But if you really need to do that, you can always revive
the LLVM C backend. The fact that nobody has bothered to revive it and keep it
up to date enough to be merged into LLVM proper, to me, is a strong indication
that few people need support for these obscure platforms.

~~~
jjnoakes
> Effort in this direction would be better spent just adding new features to
> clang (or GCC).

In your opinion. This isn't a fact. For example, I know first hand of code
that has to compile with compilers that are not in the set { gcc, clang, msvc
}. What do I do then? (Oh, and those compilers are closed-source).

> You'll end up wanting to just use clang's front end to help you

Probably, although there are other options too

> and at that point why not just modify clang itself?

The changes I make, if useful to the community, would probably end up back in
clang. But they would also remain a separate tool, because of all those other
pesky compilers I work with.

> you can always revive the LLVM C backend

I'm not sure how that helps me; not only does it seem a terribly roundabout
way to get what I'm looking for (instead of sugar -> c++ -> my c++ compiler,
you are proposing sugar -> clang -> llvm -> c -> my c compiler), but I don't
think objects produced by that pipeline would link with C++ objects produced
from my native compilers, among other issues.

> The fact that nobody has bothered to revive it and keep it up to date enough
> to be merged into LLVM proper, to me, is a strong indication that few people
> need support for these obscure platforms.

Or, is it evidence that people using more obscure platforms (we're talking
fortune 500 companies here) stick to languages that exist on their platforms
for various reasons?

~~~
solidsnack9000
Although I don't agree with this poster, it seems odd that they're being
downvoted.

------
marvel_boy
Newbie here. What are the differences with Swift Enumerations?

~~~
nathankleyn
Compared to Rust enumerations, they are one and the same. In fact, looking at
the documentation for Swift Enumerations yields this little nugget:

 _You can define Swift enumerations to store associated values of any given
type, and the value types can be different for each case of the enumeration if
needed. Enumerations similar to these are known as discriminated unions,
tagged unions, or variants in other programming languages._ [1]

Haskell and other ML family languages also have similar constructs, although
these are usually modelled using "Algebraic Data Types", or "sum types". [2]
[3]

[1]:
[https://developer.apple.com/library/content/documentation/Sw...](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Enumerations.html)

[2]:
[https://wiki.haskell.org/Algebraic_data_type](https://wiki.haskell.org/Algebraic_data_type)

[3]: [https://www.schoolofhaskell.com/school/to-infinity-and-
beyon...](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-
of-the-week/sum-types)

------
killercup
Came here to say that for the specific problem at hand (`ConnectionState`) you
should probably use session types in Rust -- but someone on /r/rust beat me to
it _and_ the author already added it to the post while I was reading! :)

------
sidlls
This is neat. I'm writing a very badly constructed toy application to get used
to Rust, and some of the differences have been interesting to note.

In this case, when I first encountered Rust's enums the first thing that came
to mind is the fact that C and C++ both offer the ability to support tagged-
unions, but certainly not as a first-class entity and definitely with a lot
more cruft, with or without safety checks.

For me the jury is still out on where the best "fit" for Rust is. I really
appreciate the enforced safety of Rust for higher-level systems applications
programming. I'm not convinced yet it won't just get in the way pointlessly
for much lower level programming (especially embedded). Perhaps that's just my
relative novice understanding of Rust, though.

~~~
bluejekyll
It depends, do you want a piece of code that will be more reliable and require
less maintenance after development?

Even after more than a year of learning the language I do find that some
things take me longer to build, but the end product is far better.

~~~
sidlls
I can write reliable code requiring less maintenance in C or C++. What you're
engaging in is language zealotry. Rust isn't a panacea.

~~~
bluejekyll
I think you're reading into the comment too much. Rust by definition, will
create a safer variant of whatever similar code you write in C or C++. This
isn't really debatable, things like bounds checking on arrays, strongly typed
error results, thread safe memory sharing semantics. These will absolutely
guarantee that in general you will have safer code in Rust.

What I said is that it might take you longer to write it in Rust, than
something similar in C/C++, but you won't have some of the guarantees you get
from the Rust semantics. So the tradeoff is up to you; write code faster, or
write code safer.

~~~
sidlls
Ah, I see now. Thank you for clarifying. Not that I agree entirely, mind, just
that your initial comment was clarified.

~~~
oldmanjay
I'm always fascinated when people decide to have an opinion on facts. That
little spark gets fanned into the flames of religion so easily, and it's no
exaggeration to say it is a primary driver in the shape of our civilization.

~~~
sidlls
I'm almost never fascinated when folks mistake opinion for fact. It's the
primary driver in many language wars, as I see it. Some come to recognize that
no language is perfect, especially their favored one. Others don't.

~~~
oldmanjay
Language wars are just another religion, so I agree.

The real tragedy of being human is that no amount of intelligence can save you
from being religious.

------
petters
Hopefully there can be a new function returning optional at some point in the
future.

~~~
hellofunk
I'm not sure I understand; in C++17, why can't you write a function that
returns std::optional?

------
ensiferum
I think the example is either naive or then contrived. There's probably some
behavioral changes that go with the state of the connection in which case
you'd be much better served by having a State interface and then
implementations for different actual states.

For example:

class State { ... }; class Connected : public State { ... };

std::unique_ptr<State> state;

~~~
dbaupp
That has downsides like, for one, requiring allocations for every state
transition, and the runtime infrastructure (plus loss of static assurances)
required to do downcasts when one needs functionality that only exists on a
specific state. For closed state spaces like this example, a discriminated
union is far more controlled and has many advantages, whereas subclassing is
often better suited to open (or large) sets of states.

------
radarsat1
The first example could have been implemented in C or C++ using a union.
Wouldn't necessarily be type-safer, but the reminder of writing
myConnection.connected.m_id instead of just myConnection.m_id is also a pretty
good way to ensuring that you remember to check if
(myConnection.m_connectionState == CONNECTED) before ever accessing
myConnection.connected. That said, having compiler errors is much better. I'd
love to be able to achieve this in pure C. One way might be to use opaque
types with accessor functions to return a pointer to the correct part of the
union according to the requested connection state, a bit like
std::variant::get_if.

------
Animats
Someone just re-invented discriminated variants from Pascal.

~~~
pjmlp
This seems to have become quite prevalent.

Apparently the majority of CS degrees don't teach history of programming
languages.

On my language design lectures in the mid-90's we had to learn all major ones,
all the way back to Fortran.

~~~
Manishearth
This really doesn't seem like a reinventing to me (from either the Rust or C++
side). "Reinventing" implies ignoring history in the way you mention.

Tagged unions are prevalent in so many languages that the designers of both
C++17 and Rust are bound to know about them. This is "borrowing" (or
"stealing" :p).

The original Rust was ML-like and had an OCaml compiler, so Rust's enums
definitely descended from those. I can't talk for C++ for sure, but like I
said it's common in so many languages that they're bound to have derived
inspiration from them.

~~~
pjmlp
I guess I was ranting a bit out of place....

