1. Background
C++ code seems to have a problem often: if the value can come from an lvalue or an rvalue, how can the object track that value? That is, if the value is kept as a reference, it cannot be bound to a temporary object. If you leave it as a value, then when it is initialized from the lvalue, an unnecessary copy will be produced.
There are several ways to deal with this situation. Using std::variant provides a good tradeoff to get expressive code.
2. Tracking value
Suppose there is a classMyClass
. Want to letMyClass
Visit a certainstd::string
. How to expressMyClass
Internal string?
There are two options:
- Store it as a reference.
- Store it as a copy.
2.1. Store references
If you store it as a reference, for example a const reference:
class MyClass { public: explicit MyClass(std::string const& s) : s_(s) {} void print() const { std::cout << s_ << '\n'; } private: std::string const& s_; };
Then we can initialize our reference with an lvalue:
std::string s = "hello"; MyClass myObject{s}; ();
It looks pretty good. But what if you want to initialize our object with an rvalue? For example:
MyClass myObject{std::string{"hello"}}; ();
Or code like this:
std::string getString(); // function declaration returning by value MyClass myObject{getString()}; ();
Then the code has undefined behavior. The reason is that the temporary string object is destroyed in the same statement that created it. When calledprint
When the string has been corrupted, using it is illegal and results in undefined behavior.
To illustrate this, ifstd::string
Replace with typeX
, and inX
The destructor prints the log:
struct X { ~X() { std::cout << "X destroyed" << '\n';} }; class MyClass { public: explicit MyClass(X const& x) : x_(x) {} void print() const { // using x_; } private: X const& x_; };
Print logs in the call location:
MyClass myObject(X{}); std::cout << "before print" << '\n'; ();
Output:
X destroyed before print
You can see that before trying to use thisX
Has been destroyed.
Complete example:
#include <iostream> #include <string> struct X { ~X() { std::cout << "X destroyed" << '\n';} }; class MyClass { public: explicit MyClass(X const& x) : x_(x) {} void print() { (void) x_; // using x_; } private: X const& x_; }; int main() { MyClass myObject(X{}); std::cout << "before print" << '\n'; (); }
2.2. Store value
Another option is to store a value. This allowsmove
Semantics move the passed temporary value into the stored value:
class MyClass { public: explicit MyClass(std::string s) : s_(std::move(s)) {} void print() const { std::cout << s_ << '\n'; } private: std::string s_; };
Now call it:
MyClass myObject{std::string{"hello"}}; ();
Generate two moves (one constructions
, one-time constructions_
) and there is no undefined behavior. In fact, even if the temporary object is destroyed,print
Instances inside the class are also used.
Unfortunately, if you return to the first call point with the lvalue:
std::string s = "hello"; MyClass myObject{s}; ();
Then we will no longer do two moves: once copy (construct s) and once move (construct s_).
More importantly, our goal is to give MyClass permission to access strings. If you make a copy, there will be an instance different from the incoming one. So they won't sync.
For temporary objects, this is not a problem, as it will be destroyed anyway, and we moved it in before so the string is still accessible. But by copying, we no longer give MyClass permission to access incoming strings.
So storing a value is not a good solution either.
3. Storage variant
Store references are not a good solution, and storing values are not a good solution. What we want to do is store the reference if the reference is initialized from an lvalue; if the reference is initialized from an rvalue, store the reference.
But the data member can only be one type: a value or a reference, right?
But, forstd::variant
, it can be any one. However, if you try to store a reference in a variable, it looks like this:
std::variant<std::string, std::string const&>
Will get a compilation error:
variant must have no reference alternative
To achieve our purpose, references need to be placed in another type; i.e. specific code must be written to handle data members. Ifstd::string
If you write such code, you cannot use it for other types.
At this point, it is best to write code in a general way.
4. General storage class
The storage needs to be a value or a reference. Since this code is now written for general purposes, non-const
Quote. Since variables cannot directly save references, they can be stored in the wrapper:
template<typename T> struct NonConstReference { T& value_; explicit NonConstReference(T& value) : value_(value){}; }; template<typename T> struct ConstReference { T const& value_; explicit ConstReference(T const& value) : value_(value){}; }; template<typename T> struct Value { T value_; explicit Value(T&& value) : value_(std::move(value)) {} };
Define storage as one of these two cases:
template<typename T> using Storage = std::variant<Value<T>, ConstReference<T>, NonConstReference<T>>;
Now you need to access the underlying value of the variable by providing a reference. Two types of access were created: one isconst
, another kind of nonconst
。
4.1. Define const access
To defineconst
Access requires that each of the three possible types inside the variable produce oneconst
Quote.
To access data in variables, usestd::visit
and standardizedoverload
Mode, this can be implemented in c++17:
template<typename... Functions> struct overload : Functions... { using Functions::operator()...; overload(Functions... functions) : Functions(functions)... {} };
To getconst
Quote, just for eachvariant
Create a:
template<typename T> T const& getConstReference(Storage<T> const& storage) { return std::visit( overload( [](Value<T> const& value) -> T const& { return value.value_; }, [](NonConstReference<T> const& value) -> T const& { return value.value_; }, [](ConstReference<T> const& value) -> T const& { return value.value_; } ), storage ); }
4.2. Define non-const access
The creation of non-const references uses the same technique exceptvariant
yesConstReference
Outside, it cannot generate non-const references. However, whenstd::visit
When accessing a variable, you must write code for every possible type of it:
template<typename T> T& getReference(Storage<T>& storage) { return std::visit( overload( [](Value<T>& value) -> T& { return value.value_; }, [](NonConstReference<T>& value) -> T& { return value.value_; }, [](ConstReference<T>& ) -> T&. { /* code handling the error! */ } ), storage ); }
Further optimization, throw an exception:
struct NonConstReferenceFromReference : public std::runtime_error { explicit NonConstReferenceFromReference(std::string const& what) : std::runtime_error{what} {} }; template<typename T> T& getReference(Storage<T>& storage) { return std::visit( overload( [](Value<T>& value) -> T& { return value.value_; }, [](NonConstReference<T>& value) -> T& { return value.value_; }, [](ConstReference<T>& ) -> T& { throw NonConstReferenceFromReference{"Cannot get a non const reference from a const reference"} ; } ), storage ); }
5. Create storage
The storage class has been defined and can be used in the example to access the incomingstd::string
, regardless of its value category:
class MyClass { public: explicit MyClass(std::string& value) : storage_(NonConstReference(value)){} explicit MyClass(std::string const& value) : storage_(ConstReference(value)){} explicit MyClass(std::string&& value) : storage_(Value(std::move(value))){} void print() const { std::cout << getConstReference(storage_) << '\n'; } private: Storage<std::string> storage_; };
(1) With lvalue when calling:
std::string s = "hello"; MyClass myObject{s}; ();
Match the first constructor and create one inside the storage memberNonConstReference
. whenprint
Function CallsgetConstReference
When, nonconst
The reference is converted toconst
Quote.
(2) Use temporary values:
MyClass myObject{std::string{"hello"}}; ();
This function matches the third constructor and moves the value to storage. getConstReference then returns the const reference of that value to the print function.
6. Summary
variant provides a very suitable solution for classic problems in c++ that track lvalues or rvalues. The code of this technique is expressive, because std::variant allows to express something very close to our intention: "A object can be a reference or a value depending on the context".
Before C++17 and std::variant, solving this problem was tricky and made the code difficult to write correctly. As languages develop, the standard library becomes more and more powerful, and we can use more and more expressive code to express our intentions.
The above is the detailed content of C++'s method of storing lvalues or rvalues in the same object. For more information about storing lvalues in the same object in C++, please pay attention to my other related articles!