📜 ⬆️ ⬇️

Using unions in constant expressions for C ++ 11

You may already be familiar with generic constant expressions . If not, you can read about them, for example, here ). In this article, I want to share my experience in using unions in constant expressions.

Associations are not very popular in OOP because of the type of security hole they open, but sometimes they provide some irreplaceable features that I personally appreciated when working with Fernando Cacciola on a draft std :: optional .

Foreword


Do you know the library Boost.Optional ? In short, boost :: optional <T> is a “compound” entity that can store any value of type T plus one additional state indicating that there is no stored value. This is such a " nullable T ".

One of the features of boost :: optional is that by initializing this object with an “empty” state, an object of type T is not created at all. Its default constructor is not even called: firstly, to increase performance, and secondly, type T may not have a default constructor at all. One way to do this is to allocate some buffer in memory large enough to save an object of type T and use an explicit constructor call only at the moment when the object needs to be created exactly.
')
template <typename T> class optional { bool initialized_; char storage_[ sizeof(T) ]; // ... }; 


This is just one of the ideas for a possible implementation. In practice, it will not work due to alignment problems - we will have to use std :: aligned_storage ; You can also use an "exclusive join" - this mechanism is described in detail in ACCU Overload # 112 . The constructors of the “empty” state and “by real value” can be implemented as follows:

 optional<T>::optional(none_t) // null-state tag { initialized_ = false; //  storage_  }; optional<T>::optional(T const& val) { new (storage_) T{val}; initialized_ = true; }; 


The implementation of the destructor, as you might have guessed, may be the following:

 optional<T>::~optional() { if (initialized_) { static_cast<T*>(storage_) -> T::~T(); } }; 


Problem number 1


In the case of std :: optional , there are some features. One of them is the fact that std :: optional <T> must be a literal type (whose objects can be used as constants at the compilation stage). One of the limitations that the Standard imposes on such types is that they must have a trivial destructor: a destructor that does nothing. And as we see in the example above, our destructor does something that is necessary, that is, our goal is generally unattainable. Although it can be achievable for special cases (when a destructor of type T is also trivial, for example, if T = int , we do not need to call its destructor). From here follows the practical definition of a trivial destructor: it is a destructor that we can not even call at all without harming the program.

Another limitation for literal types is that they must have at least one constexpr constructor . This constructor (or several constructors) will be used to create constants at the compilation stage. However, to avoid undefined behavior, the Standard defines a number of constraints on constexpr constructors and their types to ensure that all data fields in the base type are defined.

Thus, our implementation of the optional class with an appropriately sized array will not work, because in the constructor by value the array is not initialized in the member initialization list (before the constructor body). We could fill the array with zeros in the constructor of the “empty” state, but this is all the overhead at the execution stage. We will also have a similar problem in the case of std :: aligned_storage . And also we cannot use a simple exclusive join (from ACCU Overload # 112)

 template <typename T> class optional { bool initialized_; union { T storage_ }; //   }; 


This cannot be done because, if necessary, we must create an “empty” object of the class optional, either to leave the anonymous union uninitialized (which is unacceptable in constexpr-functions ), or to call the default constructor for storage_ - but this is contrary to our goal to avoid unnecessary object initialization class T.

Problem number 2


Another goal of our design is to get a function that extracts a value of type T from our object. In the implementation of boost :: optional , as well as in the proposed std :: optional , the operator "*" ( operator * ) is used to get access to the stored value. For maximum performance, we do not check the status of the parent object (for this, the user can use a separate function) and directly refer to the saved value of type T.

 explicit optional<T>::operator bool() const // check for being initialized { return initialized_; }; T const& optional<T>::operator*() const // precondition: bool(*this) { return *static_cast<T*>(storage_); } 


At the same time, we want to access operator * at the compilation stage, in which case it would be nice if the compilation failed at the time of the attempt to access the uninitialized value in the object. It may be tempting to use the method described in my other articles on calculations at the compilation stage:

 constexpr explicit optional<T>::operator bool() { return initialized_; }; constexpr T const& optional<T>::operator*() { return bool(*this) ? *static_cast<T*>(storage_) : throw uninitialized_optional(); } 


But all the same, it does not fit. We’ll actually achieve validation at compile time, but there will also be validation at run time, which will hit performance. Is it possible to leave the check at the compilation stage, but not to do it at the code execution stage?

Decision


Both of these problems are solved using a type T join and a stub:

 struct dummy_t{}; template <typename T> union optional_storage { static_assert( is_trivially_destructible<T>::value, "" ); dummy_t dummy_; T value_; constexpr optional_storage() //  ""  : dummy_{} {} constexpr optional_storage(T const& v) //    : value_{v} {} ~optional_storage() = default; //   }; 


There are special rules for using unions in constant functions and constructors. We need to initialize only one member of the union. (In fact, we cannot initialize several at the same time, since they occupy the same memory area). This member is called "active." In the event that we want to leave our repository empty, we initialize the stub. This satisfies all the formal requirements of initialization at the compilation stage, but since our dummy_t stub does not contain any data, its initialization does not take away any resources at runtime.

Second: reading (strictly speaking, “calling the lvalue-to-rvalue transformation”) of an inactive member of the union is not a constant expression and its use at the compilation stage gives us a compilation error. The following example demonstrates this:

 constexpr optional_storage<int> oi{1}; // ok constexpr int i = oi.value_; // ok static_assert(i == 1, ""); // ok constexpr optional_storage<int> oj{}; // ok constexpr int j = oj.value_; //     


Now our optional class (for types T with trivial destructors) can be implemented like this:

 template <typename T> // requires: is_trivially_destructible<T>::value class optional { bool initialized_; optional_storage<T> storage_; public: constexpr optional(none_t) : initialized_{false}, storage_{} {} constexpr optional(T const& v) : initialized_{true}, storage_{v} {} constexpr T const& operator*() // precondition: bool(*this) { return storage_.value_; } // ... }; 


The error message at the compilation stage in operator * is not ideal: it does not indicate that the object has not been initialized, but merely indicates the use of an inactive member of the union. Nevertheless, our main goal was achieved: the code with incorrect access to the value will not compile.

You can find the basic implementation of std :: optional here: github.com/akrzemi1/optional

Source: https://habr.com/ru/post/164037/


All Articles