Boost logo

Boost :

From: Eric Friedman (ebf_at_[hidden])
Date: 2003-08-29 21:09:40


Gennadiy Rozental wrote:
> > BTW, after looking at the implementation I was a bit disappointed to
> > see two copies of the storage. It seems to nullify one
> > important reason for using variants (space savings), and it generates
> > more code than a single-storage version. I know you had some rationale
> for
> > that but I don't remember what it is; I hope it's in the docs along
> > with some explanation of the expected size of the resulting object.
> >
> > Regards,
> > Dave
>
> Double storage is needed to ensure strong guaraty for variant. I argued
need
> for it before and I still believe we not nesseserily need to do that (I
> remember your post on the topic, that strong guaranty is a "incorrect"
goal
> to pursue for the variant).

Yes, it is true that double storage provides strong guarantee for variant.
But that is not the point: double storage is needed to ensure even the basic
guarantee!

First let me acknowledge that the above rests on certain technical
assumptions about variant's invariants (sorry about the tongue twister).
Namely, it assumes that "never empty" is an invariant. Thus, for instance,
boost::variant<int, std::string> *always* contains either an int or a
std::string, and that these types maintain their own invariants.

There are important reasons I have built variant on this assumption. Suppose
I have a variant v1, with content of type T1, and a variant v2, also
with content of type T1. Then the assignment v1 = v2 is perfectly
straightforward: use T1::operator=. Obviously no double storage is needed
for this case.

But suppose I have a variant v3, with content of a different type (call it
T3). Then the assignment v1 = v3 is far more complicated (we can't use
T1::operator=) and, without double storage, far more dangerous. The single
storage implementation behaves as follows:

    destroy v1 content
    copy v3 content into v1

The destruction will of course always occur because variant requires nothrow
destructors from all of its bounded types. But what if the copy fails? Then
the variant has no meaningful content whatsoever, since there is no
guarantee we can restore the original content or, worse still, even
construct new content of one of variant's bounded types.

To see why this is a problem, we need to look back at the motivation for
variant. Apart from efficiency, one of the significant advantages of using
variant instead of boost::any is improved type safety, specifically at
compile-time. Indeed, the visitation mechanism forces the user to handle
every possible type of content. If we then go ahead and allow the
possibility of the above "empty" state (to be distinguished from
boost::empty content), we greatly complicate these semantics. Are we to say
that anytime a variant is involved in a failed assign/swap operation,
visitation will subsequently fail -- but only at runtime?

Some may find this behavior acceptable. Personally, I'm not totally opposed,
as there are alternative (albeit less elegant) options. But I think before
throwing away double storage, we need to fully understand what else will
leave with it.

I invite (and would appreciate) feedback, as this is an important issue.

Thanks,
Eric

P.S. Any type with a nothrow move constructor (which includes all types with
a nothrow copy constructor) is not included in the double storage. Of
course, except for types with nothrow copy constructors, this optimization
is largely useless until the advent of a Boost.Move library. I do plan to
submit one for inclusion in the Boost 1.32 release, but that presumably will
not be for some months from now.


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk