Search code examples
c++boostboost-variant

What does boost::has_nothrow_constructor actually do for boost::variant?


In the context of boost::variant, I understand that boost::has_nothrow_copy keeps copying from allocating the object being overwritten onto the heap incase the copy throws and the original needs to be recovered (though I'm a little surprised that it doesn't do a move).

However, I'm unclear as to the purpose of boost::has_nothrow_constructor. Why does it need this? In the docs it states:

Enabling Optimizations

...

  • If any bounded type is nothrow default-constructible (as indicated by boost::has_nothrow_constructor), the library guarantees variant will use only single storage and in-place construction for every bounded type in the variant. Note, however, that in the event of assignment failure, an unspecified nothrow default-constructible bounded type will be default-constructed in the left-hand side operand so as to preserve the never-empty guarantee.

This seems to indicate that without the specialization to std::true_type, this would result in using either multi storage or non-in-place construction. What does that even mean?

NOTE: I actually found that this pdf was easier to read because it had more examples.


Solution

  • The answer is in your quoted piece of Boost.Variant documentation.

    Boost.Variant maintains a never-empty guarantee, meaning that no matter what happens during boost::variant's lifetime, it is guaranteed to contain exactly one object of one of the types listed in its template parameters. This poses a problem with assignment to a boost::variant - what happens if assignment fails with an exception?

    For example, let's consider the following piece of code:

    std::string str("Some very long string");
    boost::variant< int, std::string > var(10);
    var = str;
    

    Initially, var contains a value 10 of type int. Then it is assigned a value of type std::string, and for the sake of this example let's assume this assignment fails with an exception (e.g. std::bad_alloc).

    Since the original stored value was not of type std::string, that value needs to be destroyed. int has trivial destructor, so destroying it is a no-op. But then the storage it occupied is reused by the newly constructed std::string, which is in this case copy-constructed from str. As we established above, this copy constructor throws, and there is no active value left in var - the string failed to construct and the previous int is already lost.

    One solution could be to use a heap-allocated storage for constructing the string first, before destroying the currently stored value in the variant. If the construction succeeds, then the stored value is destroyed, and the variant stores a pointer to the newly constructed value internally. And that is what boost::variant does, with a few exceptions.

    First, if the type of the value that is being assigned has a non-throwing copy constructor then this whole problem is non-existent - the copy construction is guaranteed not to fail, so it is safe to destroy the original value before the copy. This is detected by boost::has_nothrow_copy. This isn't our case in this example, as std::string's copy constructor clearly can throw an exception.

    Second, if any of the types that are listed in boost::variant template parameters has a non-throwing default constructor, as determined by boost::has_nothrow_constructor, then boost::variant can recover from a failed assignment by default-constructing a value of one of such types. Boost.Variant doesn't specify which of the types will be chosen if multiple types have a non-throwing default constructor, except that a special type boost::blank will be preferred, if listed. This optimization matches our case, as the default constructor of int is trivial and never throws. So the behavior of the assignment in our example will be as follows:

    1. Destroy the currently stored value of type int.
    2. Copy-construct a new value of type std::string in the internal storage of var.
    3. If the copy constructor throws and exception, default-construct a value of type int in the internal storage and propagate the exception to the caller.

    As a result, the value of var becomes an indeterminate value of type int (because the default constructor of int does not actually initialize it).