SoFunction
Updated on 2025-04-13

C++ method to store lvalues ​​or rvalues ​​in the same object

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 letMyClassVisit a certainstd::string. How to expressMyClassInternal 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 calledprintWhen the string has been corrupted, using it is illegal and results in undefined behavior.

To illustrate this, ifstd::stringReplace with typeX, and inXThe 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 thisXHas 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 allowsmoveSemantics 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,printInstances 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::stringIf 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-constQuote. 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 defineconstAccess requires that each of the three possible types inside the variable produce oneconstQuote.

To access data in variables, usestd::visitand standardizedoverload Mode, this can be implemented in c++17:

template<typename... Functions>
struct overload : Functions...
{
    using Functions::operator()...;
    overload(Functions... functions) : Functions(functions)... {}
};

To getconstQuote, just for eachvariantCreate 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 exceptvariantyesConstReferenceOutside, it cannot generate non-const references. However, whenstd::visitWhen 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. whenprintFunction CallsgetConstReferenceWhen, nonconstThe reference is converted toconstQuote.

(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!