Value categories – [l, gl, x, r, pr]values

So you have heard about lvalues and rvalues for sure, but there actually are also xvalues, prvalues and glvalues. It’s easy to get lost in it, so let’s have a look at what actually are those things.

types of expression according to the standard

The picture shows the division of the value categories defined in the draft standard. In this particular case, it’s here

So, first of all, we can see, that glvalue and rvalue are nothing more than the containers for more concrete value categories. My guess terms glvalues and rvalues were added to the standard for convenience to avoid repeating words like “lvalue and xvalue” and “xvalue and prvalue”.

Glvalues

Let’s first focus on the glvalues and let’s understand what this actually means, that the expression is a glvalue. So first what actually standard says about the glvalues is that glvalues expressions evaluation determines the identity of an object. In other words, it means, that results of such expressions refer to the concrete object (those are just references to the objects).

How I like to think about about glvalues is: if expression returns reference (T&), or refers to the variable by it’s name, or rvalue reference (T&&) to the object it’s a glvalue expression. Let’s see examples of the glvalues. For the given definitions:

int& foo(){
  static int i=0;
  return ++i;
}

int a;

struct Bar{
  int m;
};

Bar bar;

Following expressions are glvalues:

"foo()" // expression because returned value refers to static 'i' variable. The return type is int&
"a" // expression as it refers to the 'a' can be assigned to int&
"std::move(a)" // expression as it still refers to the a variable. return type is int&&
"bar" // refers to the bar variable. Can be assigned to Bar&
"bar.m" // refers to the m member inside the bar object return type. Can be assigned to int&

But glvalues are divided into lvalues and xvalues, so let’s first see what xvalues are since lvalues are defined as all glvalues which are not xvalues ๐Ÿ™‚

So xvalues as already mentioned are glvalues, so still if expression yields a xvalue reference, then this reference references some concrete instance of the object. The xvalues have one more important attribute: citing the standard: An xvalue is a glvalue that denotes an object or bit-field whose resources can be reused (usually because it is near the end of its lifetime) .

So what xvalue means in practice? Well, xvalue used to be an lvalue, but for some reason, it was marked, that its resources can be reused or in other words, the object should be moved instead of copied. That kind of additional marking to the type can be achieved when calling std::move function on the object. Another way of getting the xvalue is through temporary materialization conversion, but we will talk about it in the conversion section.

One more time my way of thinking about xvalues is, that if expression returns type of rvalue reference (T&&), then expression is xvalue. We can get expression of xvalue category type with following rules:

//any call of the function (member function included), which result type is rvalue reference
struct Foo{};
Foo&& bar(); 

int main(){
  bar(); // "bar()" is the xvalue expression 
}
 //cast to the rvalue reference. Usually with std::move() function

struct Foo{/* definition */};

int main() {
  Foo a;
  std::move(a); // "std::move(a)" is xvalue expression since move casts the a to rvalue
  static_cast<Foo&&>(a); // "static_cast<Foo&&>(a)" is also xvalue expressions because of explicit cast.
}
 //in case of arrays the "xvalue property" is "propagated" also to the subscript operator

int main(){
  Foo arr[10] = {};
  std::move(arr)[0]; // "std::move(arr)[0]" yields rvalue reference to the first element in the array
}
// same thing happens with the access to the class members.

template <typename T>
struct Foo{
  T member;
};

int main(){
  using non_reference_type=/**/;
  Foo<non_reference_type> a{};
  std::move(a).member; //yields rvalue reference to the member
}
// should be no surprise that similar thing happens with pointers to members

int main(){
  int Foo<int>::* pointer = &Foo<int>::member;
  Foo<int> foo{};
  std::move(foo).*pointer; //yields rvalue reference to the member on same conditions as with the member access case.
  return 0;
}

(offtopic) : If you didn’t have a possibility to see pointers to members in action, then this syntax must be a pain in your eyes. My apologies for that. What you need to know beside the syntax itself is that pointer to member is actually a simple offset from the beginning of the object to the actual member. Thus there is no need for a concrete object existence (only definition is needed) before saving this pointer to the variable.

Some other examples from the standard about xvalues can be found below, where for given definitions:

struct A {
  int m;
};
A&& operator+(A, A);
A&& f();

A a;
A&& ar = static_cast<A&&>(a);

Following expressions are xvalues:

  • f()
  • f().m
  • static_ยญcast<A&&>(a)
  • a + a

So once we know what glvalues are and what xvalues are we also know what lvalues are, since it’s all what is left after subtracting glvalues and xvalues sets. Meaning, that lvalue expressions, are expressions, that yield lvalue references (T&) or refer (by name) to an existing variable.

As a summary, we should say, that all expressions, that are glvalues actually yield a handle to some variable with some value. Thus it’s not possible to have the expression of glvalue type, that would have (const volatile) void type (it’s not possible to have object of type void). Glvalues are references to the objects.

Rvalues

Rvalues as can be seen on the image is a set containing all xvalues and also prvalues. Since we know what xvalues are, we need to have a look at prvalues a little bit closer.

Prvalues are “pure” rvalues. The result of the prvalue expression is a value, that can be stored in the variable. At the same time, it’s possible to have the prvalue of type (const volatile) void. The intuition for that is, that prvalue is different from lvalue and xvalue with the fact that it does not refer to any kind of object, but it is just a value. Meaning in short, that if expression yields type, which is a value (T) not a reference (T& or T&&) and expression is not a name of a variable, then it’s a prvalue expression. In other words prvalues are temporaries.

I think it’s best to explain the prvalues on the examples.

struct Foo {};
Foo(); // prvalue

Foo test(){/*implementation*/};
test(); // prvalue

Type completeness requirements

The glvalues types (lvalue and xvalue) have different characteristics than the prvalues. Because of their nature (being the handle of the object) the expression can yield a value of incomplete type. Since prvalues are values itself they need to have a complete type (or void type). Let’s consider the following example, where the function does not need a complete type at all.

template <typename T>
T& first_val(T& first, T& second){return first;}

The above function does not need a complete definition of type T, that it operates on. Let’s consider following usage of the function:

// separate translation unit
struct foo {

};

foo& get_first(){
    static foo foo1;
    return foo1;
}

foo& get_second(){
    static foo foo2;
    return foo2;
}
//the main function, does not see another translation unit
struct foo;

foo& get_first();
foo& get_second();

template <typename T>
T& first_value(T& first, T& second){
    return first;
}

int main() {
    auto& first = get_first();
    auto& second = get_second();

    auto& result = first_value(first, second);

    return 0;
}

The whole program compiles and can be run even though in the main translation unit the type foo is not complete. It’s possible because nowhere in the program foo is used as prvalue plus there are no operations directly invoked on the foo type, that would require it’s definition.

Let’s consider the same example, but with changed return type of the first_value template function:

//the main function, does not see another translation unit
struct foo;

foo& get_first();
foo& get_second();

template <typename T>
T first_value(T& first, T& second){
    return first;
}

int main() {
    auto& const first = get_first();
    auto& const second = get_second();

    auto& const result = first_value(first, second);

    return 0;
}

Now the return value of the first_value function is prvalue, since it’s not a handle to any of the objects. The program now will fail to compile as the prvalue expressions need complete definition of the type they yield.

On my environment the compilation results in following message:

error: invalid use of incomplete type 'struct foo'
error: return type 'struct foo' is incomplete

Expression types conversions

To make developers life easier, there are lots of conversions defined, when types of expressions do not match regarding its value category. The conversions, that will apply might be from glvalue to prvalue expressions or prvalue to xvalue conversions

Glvalue to prvalue conversion

Glvalue to prvalue happens very often and there are many such conversions defined in the C++. They are:

Array to pointer conversion:

// in case you expect the prvalue of type pointer to array
// the glvalue of type array is implicitly cast to the prvalue of 
// required pointer type

//here str is accepted as the prvalue of type pointer to const char
void printme(const char* str){/*definition*/}

int main(){
  char str[] = {'a', 'b', 'c', 'd', '\0'};
  printme(str); 
// compiles fine. the glvalue of type char[5] is implicitly converted to
// rvalue of type pointer to char.
}

The above conversion is called array to pointer coversion. Another type of similar conversion is function to pointer conversion. In such conversion, the glvalue expression, that refers to the function of type T implicitly gets converted to the pointer to function. Let’s see the example:

void foo(){} // it's a function
void foo2(void(*)()) // a function that accepts prvalue pointer to function

void main(){
  foo; // it's a glvalue expression, that refers to the function foo
  foo2(foo); // foo glvalue expression gets implicitly converted to the pointer to function prvalue 
};

The last conversion from glvalue expression to rvalue expression is simply called lvalue to rvalue conversion (though the name is not precise) . Let’s have a look at the lvalue to rvalue conversion in detail:

First of all, the conversion does not apply to the array and function types as this would otherwise conflict the array to pointer and function to pointer conversions. Another precondition for such conversion is, that when such conversion takes place, the T must be a complete type. This requirement comes from already mentioned prvalues expression requirements. Since prvalue represents the value, the complete type of this value must be known.

Once the preconditions are met the conversion is following:

  • For non-class types (class types: class, struct, union) the resulting type is cv unqualified. The resulting object is obtained by copy initialization
  • Otherwise (for class types) the resulting type preserves it’s const and volatile qualifications.

I personally think it’s the most often used conversion (no statistics about it) and usually noone really is aware of this conversion. Let’s see the example of the expression conversion:

void foo(Bar value);
Bar bar;
foo(bar); // here bar is lvalue but get's converted to the prvalue with copy initialization

Looking at above example it’s easy to realize, that almost every function call in the program relies on the lvalue to rvalue conversion.

Prvalue to xvalue conversion.

There is also vice versa conversion, that is applied if you are trying to pass the prvalue in the place, where you are actually expecting the glvalue. In such a case the conversion is not enough, because except for the type conversion there must be object instantiation, since glvalue needs to refer to the object.

Such C++ feature, that actually does the object instantiation and casts prvalue type to the xvalue (that belongs to the glvalue group) is called temporary materialization conversion. 

When needed, the temporary materialization conversion creates a temporary variable, that is initialized with the value, that prvalue expression holds, and marks that temporary variable as a xvalue. The reason its xvalue is because the temporary variable will soon expire, thus it’s resources are ready to be reused. Thanks to this conversion we can do things like:

struct Foo{int member;};
Foo().member; // Foo() is a temporary materialization conversion, since access to the member requires object itself

void bar(const Foo& foo);
bar(Foo());

We can see that in action in detail when we try to get the address of the variable. Let’s see the example. If we have got a prvalue expression, we cannot take the address of the result of such expression, because there is no variable, that stands behind the value ( it’s a temporary ). In the following example, even if conversion is applied, the program will still be ill-formed, since you can take the address of the lvalue only (xvalue is not enough)

struct Foo{};
Foo* ptr = &Foo(); // ill-formed

In such case clang produces following error:

error: taking the address of a temporary object of type 'Foo'

But we can take address of the result of such expression after temporary materialization conversion:

void foo(Foo&& test){
  std::cout << "ptr to test: " << &test << std::endl;
}

int main()
{
  foo(Foo());
}

So we can see, that we successfully have taken the address of the variable, thus there must exist some variable , that was “materialized” by the C++ rules.

Expression types and bit fields

The bit fields are a bit special in the C++ world. As you cannot take the pointer to the bit-field itself. That’s something natural as bitfields can occupy less memory than one byte, which is the smallest addresable unit in C++ world.

But the fact that you cannot take the address to the bit-field doesn’t mean you cannot have the glvalue expression to the bit field.

This example also shows, that expression category does not always have one to one correlation to the type they return.

struct Foo{
  // definition
  char a:3;
};

Foo().a;// it is actually a glvalue
Foo foo;
foo.a = 0; // foo.a is lvalue, you can assign it.
auto i = foo.a; // foo.a is a bit-field, so i should be deduced to also be the bit-field, but as bit-fields are automatically converted to bit-field's type, then i is of type character. The statement is valid.

auto& j = foo.a; // this actually is ill-formed. you cannot have modifiable glvalue to the bit-field.

const auto& k = foo.a; // it's fine k refers to foo.a, but since it's const compiler is free to make a copy of the bitfield.

That’s also one of few differences between the pointers and references ๐Ÿ™‚ it’s not possible to take the pointer to the bitfield, be it const or not, but you can take a const reference to the bitfield.

So you can see, that you can have lvalue expression pointing to the bitfield, since you can assign to them. It’s not on the other hand possible to save the reference to the bitfield to the variable manually. This is the slight difference between the expression type and type itself.

Bibliography:


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.