C++98/11/17 Expression Categories

target

Can the following code be compiled and passed, and run as expected?(Click to expand)
#include <utility>
#include <type_traits>

namespace cpp98
{

struct A { };
A func() { return A(); }

int main()
{
    int i = 1;
    i = 2;
    // 3 = 4;
    const int j = 5;
    // j = 6;
    i = j;
    func() = A();
    return 0;
}

}

namespace cpp11
{

#define is_lvalue(x)  std::is_lvalue_reference<decltype((x))>::value
#define is_prvalue(x) !std::is_reference<decltype((x))>::value
#define is_xvalue(x)  std::is_rvalue_reference<decltype((x))>::value
#define is_glvalue(x) (is_lvalue(x) || is_xvalue(x))
#define is_rvalue(x)  (is_xvalue(x) || is_prvalue(x))

void func();
int non_reference();
int&& rvalue_reference();
std::pair<int, int> make();

struct Test
{
    int field;
    void member_function()
    {
        static_assert(is_lvalue(field), "");
        static_assert(is_prvalue(this), "");
    }
    enum Enum
    {
        ENUMERATOR,
    };
};

int main()
{
    int i;
    int&& j = std::move(i);
    Test test;

    static_assert(is_lvalue(i), "");
    static_assert(is_lvalue(j), "");
    static_assert(std::is_rvalue_reference<decltype(j)>::value, "");
    static_assert(is_lvalue(func), "");
    static_assert(is_lvalue(test.field), "");
    static_assert(is_lvalue("hello"), "");

    static_assert(is_prvalue(2), "");
    static_assert(is_prvalue(non_reference()), "");
    static_assert(is_prvalue(Test{3}), "");
    static_assert(is_prvalue(test.ENUMERATOR), "");

    static_assert(is_xvalue(rvalue_reference()), "");
    static_assert(is_xvalue(make().first), "");

    return 0;
}

}

namespace reference
{

int&& rvalue_reference()
{
    int local = 1;
    return std::move(local);
}

const int& const_lvalue_reference(const int& arg)
{
    return arg;
}

int main()
{
    auto&& i = rvalue_reference();        // dangling reference
    auto&& j = const_lvalue_reference(2); // dangling reference
    int k = 3;
    auto&& l = const_lvalue_reference(k);
    return 0;
}

}

namespace auto_decl
{

int non_reference() { return 1; }
int& lvalue_reference() { static int i; return i; }
const int& const_lvalue_reference() { return lvalue_reference(); }
int&& rvalue_reference() { static int i; return std::move(i); }

int main()
{
    auto [s1, s2] = std::pair(2, 3);
    auto&& t1 = s1;
    static_assert(!std::is_reference<decltype(s1)>::value);
    static_assert(std::is_lvalue_reference<decltype(t1)>::value);

    int i1 = 4;
    auto i2 = i1;
    decltype(auto) i3 = i1;
    decltype(auto) i4{i1};
    decltype(auto) i5 = (i1);
    static_assert(!std::is_reference<decltype(i2)>::value, "");
    static_assert(!std::is_reference<decltype(i3)>::value, "");
    static_assert(!std::is_reference<decltype(i4)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(i5)>::value, "");

    auto n1 = non_reference();
    decltype(auto) n2 = non_reference();
    auto&& n3 = non_reference();
    static_assert(!std::is_reference<decltype(n1)>::value, "");
    static_assert(!std::is_reference<decltype(n2)>::value, "");
    static_assert(std::is_rvalue_reference<decltype(n3)>::value, "");

    auto l1 = lvalue_reference();
    decltype(auto) l2 = lvalue_reference();
    auto&& l3 = lvalue_reference();
    static_assert(!std::is_reference<decltype(l1)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(l2)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(l3)>::value, "");

    auto c1 = const_lvalue_reference();
    decltype(auto) c2 = const_lvalue_reference();
    auto&& c3 = const_lvalue_reference();
    static_assert(!std::is_reference<decltype(c1)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(c2)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(c3)>::value, "");

    auto r1 = rvalue_reference();
    decltype(auto) r2 = rvalue_reference();
    auto&& r3 = rvalue_reference();
    static_assert(!std::is_reference<decltype(r1)>::value, "");
    static_assert(std::is_rvalue_reference<decltype(r2)>::value, "");
    static_assert(std::is_rvalue_reference<decltype(r3)>::value, "");

    return 0;
}

}

namespace cpp17
{

class NonMoveable
{
public:
    int i = 1;
    NonMoveable(int i) : i(i) { }
    NonMoveable(NonMoveable&&) = delete;
};

NonMoveable make(int i)
{
    return NonMoveable{i};
}

void take(NonMoveable nm)
{
    return static_cast<void>(nm);
}

int main()
{
    auto nm = make(2);
    auto nm2 = NonMoveable{make(3)};
    // take(nm);
    take(make(4));
    take(NonMoveable{make(5)});
    return 0;
}

}

int main()
{
    cpp98::main();
    cpp11::main();
    reference::main();
    auto_decl::main();
    cpp17::main();
}

C++98 Expression Category

Each C++ expression has one type: 42 is int, int i; and (i) is int&.These types fall into several categories.In C++98/03, each expression is either left or right.

An lvalue is an expression that points to a value that is actually stored in memory or in a register."l" means "left-hand side" because only lvalue in C can be written to the left of the assignment operator.In contrast, the right value (rvalue,'r'refers to'right-hand side') can only appear to the right of the assignment operator.

There are exceptions, such as const int i; I is left but cannot appear to the left of the assignment operator.In C++, the rvalue of the class type can appear on the left side of the assignment operator. In fact, assignment here is a call to the assignment operator function, which is different from assignment of the basic type.

lvalue can be understood as the value of an acceptable address, variables, dereferences to pointers, calls to functions whose return type is a reference type, and so on, are all lvalues.Temporary objects are rvalue s, including literal quantities and function calls with return types that are not reference types.The exception is the string literal, which is an unmodifiable left value.

An lvalue is required on the left and a rvalue on the right of the assignment operator. If an lvalue is given, the lvalue is implicitly converted to a rvalue.This process is taken for granted.

motivation

C++11 introduces right-value reference and move semantics.The reference to the right value returned by the function, as the name implies, should behave like the right value, but this will break many of the existing rules:

  • rvalue is anonymous and does not necessarily have storage space, but the right-value reference points to a specific object in memory that needs to be maintained.

  • rvalue's type is deterministic, must be complete, static type is the same as dynamic type, and right-value reference can be incomplete or multiform;

  • Rvalues of non-class types do not have cv modifiers (const and volatile), but right-value references can, and modifiers must be preserved.

This challenges the traditional lvalue/rvalue dichotomy, and the C++ Committee has a choice:

  • Maintain the right value reference as rvalue, adding some special rules;

  • To classify the right value reference as lvalue, add some special rules;

  • Refine expression categories.

These issues are just the tip of the iceberg; history has chosen a third option.

C++11 Expression Categories

C++11 was presented Expression category Concept.Although it's called a value category, categories are divided into expressions rather than values, so I've translated it into an expression category from the beginning of the title.The C++ standard definition expression is:

An expression is a sequence of operators and operands that specifies a computation. An expression can result in a value and can cause side effects.

Each expression is one of three categories: lvalue, xvalue, and prvalue, known as the primary category.There are also two mixed categories: lvalue and xvalue collectively refer to the canonical left value, and xvalue and prvalue collectively refer to the right value.

#define is_glvalue(x) (is_lvalue(x) || is_xvalue(x))
#define is_rvalue(x)  (is_xvalue(x) || is_prvalue(x))

C++11 defines these categories as follows:

  • lvalue specifies a function or an object;

  • Xvalue (eXpiring vavlue) also points to objects, usually near the end of their life cycle; some expressions involving right-value references result in xvalue;

  • gvalue (generalized lvalue) is an lvalue or xvalue;

  • rvalue is xvalue, temporary object, or their child object, or has no value associated with the object;

  • Is prvalue (pure rvalue) a rvalue of xvalue?

This definition is not very clear.Specifically, lvalue includes: (Click to expand)
  • The names of variables, functions, and data members, including variables of right-value reference type, are also lvalue.

    int i;
    int&& j = std::move(i);
    static_assert(is_lvalue(j), "");
    static_assert(std::is_rvalue_reference<decltype(j)>::value, "");
    
  • A function call or overloaded operator expression whose return type is either the left-value reference type or the right-value reference type of the function;

  • Built-in assignment, compound assignment, pre-auto-increase, pre-auto-decrease operator expressions;

  • Built-in array subscript expressions a[n] and p[n] (a is array type, P is pointer type), A is a n array lvalue;

  • a.m, unless m is an enumeration member, or a non-static member function, or a is a rvalue and M is a non-static data member of a non-reference type;

  • P->m unless m is an enumerated member or a non-static member function;

  • a.*mp, A is an lvalue, MP is a data member pointer;

  • P->*mp, MP is the data member pointer;

  • Comma expression, the second operand is lvalue;

  • Conditional operator a? B: c, which is very complex here rule , for example, when b and c are of the same type of lvalue;

  • String literal quantity;

  • Explicitly converts to the left-value reference type or the right-value reference type of the function.

The nature of lvalue:

  • Same as glvalue;

  • The built-in address operator works on lvalue;

  • The modifiable lvalue can appear to the left of the built-in assignment operator;

  • Can be used to initialize a left value reference.

prvalue includes:
  • Literal quantities other than strings;

  • Function call or overloaded operator expression whose return type is a non-reference type;

  • Built-in arithmetic, logical, comparative, and address operator expressions;

  • a.m or p->m, m is an enumerated member or a non-static member function (see below);

  • a.*mp or p->*mp, MP is the member function pointer;

  • Comma expression, the second operand is rvalue;

  • Partial cases of conditional operator a? b: c, such as b and C being the same type of prvalue;

  • Explicit conversion to non-reference type;

  • this pointer;

  • Enumerate members;

  • Untyped template parameter unless it is a left-value reference type;

  • lambda expression.

The nature of prvalue:

  • Same as rvalue;

  • Cannot be polymorphic;

  • prvalue, which is not a class type and is not an array, has no cv modifier, even if it is written;

  • Must be a complete type;

  • Cannot be an abstract type or its array.

xvalue includes:
  • Function call or overloaded operator expression whose return type is the right-value reference type;

  • The built-in array subscript expression a[n], A is a n array rvalue;

  • a.m, A is a rvalue and M is a non-static data member of a non-reference type;

  • a.*mp, A is a rvalue, MP is a data member pointer;

  • Partial cases of conditional operators a? b: c, such as b and C being the same type of xvalue.

The nature of xvalue;

  • Same as rvalue;

  • Same as glvalue.

The nature of glvalue:

  • Can be implicitly converted to prvalue;

  • It can be polymorphic;

  • It can be an incomplete type.

The nature of rvalue:

  • The built-in address operator does not work on rvalue;

  • Cannot appear to the left of a built-in or compound assignment operator;

  • Can be bound to const left value reference (see below);

  • Can be used to initialize right-value references (see below);

  • If a function has two overloads, a right-value reference parameter and a const left-value reference parameter, when a rvalue is passed in, the right-value reference overload is called.

There are also special classifications:

  • For non-static member functions MF and its pointer P mf, a.mf, p->mf, a. *p MF and p->*p MF are classified as prvalue, but they are not regular prvalue, but pending (upcoming) member function call and can only be used for function calls;

  • Function calls that return voids, type swaps to voids, and throw statements are void expressions and cannot be used to initialize references or function parameters.

  • The smallest addressing unit in C++ is bytes, so bit fields cannot be bound to non-const left-value references; const left-value references and right-value references can bind positioning fields, which point to a copy of a bit field.

Finally, five categories have been introduced.Expressions can be divided into three categories: lvalue, xvalue and prvalue. lvalue and prvalue are similar to lvalue and rvalue in C++98, while xvalue is entirely a right-value reference and has both glvalue and rvalue properties.In addition to this three-class method, expressions can also be divided into lvalue and rvalue, the main difference between them is whether the address can be taken or not; they can also be divided into glvalue and prvalue, the main difference between them is whether there is an entity, glvalue has an entity, so the original object can be modified, xvalue is often squeezed residual value.

Reference Binding

Let's diverge a little bit to see two features related to expression classification.

Reference bindings are of the following types:

  • The left value refers to the binding lvalue, and the cv modifier can only be more or less;

  • Right-value references can bind rvalue s, and we usually do not add cv modifiers to right-value references.

  • A const left-value reference can bind rvalue.

The left value refers to the binding lvalue as if it were natural, and there is nothing to look at.However, rvalues are temporary objects, and binding to references means that if you continue to use them, their life cycle will be affected.Normally, rvalue's life cycle extends to the declaration cycle of a binding reference with the following exceptions:

  • The temporary object returned by the return statement is destroyed immediately after the return statement ends, and such a function always returns a dangling reference.

  • The life cycle of a temporary object bound to a reference in the initialization list extends only to the end of the constructor - a bug that was fixed in C++14;

  • The life cycle of a temporary object bound to a function parameter extends to the end of the expression in which the function call is made, and returning the parameter as a reference results in an empty reference.

  • The life cycle of a temporary object bound to a reference in a new expression extends only to the end of an expression containing new, and does not follow that object.

In short, the life cycle of a temporary variable can only be extended once.

#include <utility>

int&& rvalue_reference()
{
    int local = 1;
    return std::move(local);
}

const int& const_lvalue_reference(const int& arg)
{
    return arg;
}

int main()
{
    auto&& i = rvalue_reference();        // dangling reference
    auto&& j = const_lvalue_reference(2); // dangling reference
    int k = 3;
    auto&& l = const_lvalue_reference(k);
}

rvalue_reference returns a reference to a local variable, so i is an empty reference; 2 is bound to const_Lvalue_On the reference parameter arg, the extended life cycle after the function returns reaches the end point, so j is also a dangling reference; k is not created by a temporary object at all during the process of passing parameters, so l is not a dangling reference, it is a const left-value reference pointing to k.

auto and decltype

Beginning with C++11, the auto keyword is used to derive types automatically, using Template parameter derivation Rule: If the copy list is initialized, the corresponding template parameter is std::initializer_List<T>, otherwise replace auto with T.As for the detailed rules for deriving template parameters, what you have to say is a big deal.

Okay, that's not our focus.Before we get to the point, let's look at the decltype.

decltype is used to declare a type ("declare type"), which has two syntaxes:

  • decltype(entity);

  • decltype(expression).

First, if the parameter to decltype is an identifier or class member without parentheses, decltype produces the type of the entity; if it is Structured Binding Produces a quoted type.

Second, the decltype parameter is not able to match any expression of the first type, which is of type T, and is discussed according to its expression category:

  • If it's xvalue, it generates T && - #define is_Xvalue(x) std::is_Rvalue_Reference <decltype((x)>:: value;

  • In the case of lvalue, T&-#define is_is generatedLvalue(x) std::is_Lvalue_Reference <decltype((x)>:: value;

  • If it's prvalue, produce T - #define is_Prvalue(x)! Std:: is_Reference <decltype((x)>:: value.

Therefore, decltype(x) and decltype((x)) usually produce different types.

For auto without reference modifiers, the expression class of the initializer is erased, and a new syntax decltype(auto) is introduced for this C++14, resulting in decltype(expr), where expr is the initializer.For local variables, you can preserve the expression category by adding a pair of parentheses to the right of the equal sign.

#include <utility>
#include <type_traits>

int non_reference() { return 1; }
int& lvalue_reference() { static int i; return i; }
const int& const_lvalue_reference() { return lvalue_reference(); }
int&& rvalue_reference() { static int i; return std::move(i); }

int main()
{
    auto [s1, s2] = std::pair(2, 3);
    auto&& t1 = s1;
    static_assert(!std::is_reference<decltype(s1)>::value);
    static_assert(std::is_lvalue_reference<decltype(t1)>::value);

    int i1 = 4;
    auto i2 = i1;
    decltype(auto) i3 = i1;
    decltype(auto) i4{i1};
    decltype(auto) i5 = (i1);
    static_assert(!std::is_reference<decltype(i2)>::value);
    static_assert(!std::is_reference<decltype(i3)>::value);
    static_assert(!std::is_reference<decltype(i4)>::value);
    static_assert(std::is_lvalue_reference<decltype(i5)>::value);

    auto n1 = non_reference();
    decltype(auto) n2 = non_reference();
    auto&& n3 = non_reference();
    static_assert(!std::is_reference<decltype(n1)>::value, "");
    static_assert(!std::is_reference<decltype(n2)>::value, "");
    static_assert(std::is_rvalue_reference<decltype(n3)>::value, "");

    auto l1 = lvalue_reference();
    decltype(auto) l2 = lvalue_reference();
    auto&& l3 = lvalue_reference();
    static_assert(!std::is_reference<decltype(l1)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(l2)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(l3)>::value, "");

    auto c1 = const_lvalue_reference();
    decltype(auto) c2 = const_lvalue_reference();
    auto&& c3 = const_lvalue_reference();
    static_assert(!std::is_reference<decltype(c1)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(c2)>::value, "");
    static_assert(std::is_lvalue_reference<decltype(c3)>::value, "");

    auto r1 = rvalue_reference();
    decltype(auto) r2 = rvalue_reference();
    auto&& r3 = rvalue_reference();
    static_assert(!std::is_reference<decltype(r1)>::value, "");
    static_assert(std::is_rvalue_reference<decltype(r2)>::value, "");
    static_assert(std::is_rvalue_reference<decltype(r3)>::value, "");
}

Variables defined with auto are of type int, regardless of the reference to the return type of the function or the const modifier; variables defined with decltype(auto) are of the same type as function return types; auto &&is Forward References , n3 type is int &&, the rest is the same as decltype(auto).

C++17 Expression Categories

It is well known that compilers often perform NRVO (named return value optimization) to reduce one movement or copy of the return value of a function.However, this is what the C++ standard says the compiler can do, but there is no guarantee that the compiler will do so, so the client cannot make any assumptions about it and needs to provide a copy or move constructor, although they may not be called.However, not all cases provide a move constructor, and even a move constructor is not necessarily just a pointer exchange.In summary, it is very formalistic to provide a mobile constructor with the knowledge that it will not be called.

So C++17 specifies Copy Omitting Make sure that even if the copy or move constructors have an observable effect, they are not invoked and that the object being copied or moved is constructed directly at the target location:

  • In the return expression, the operand ignores the prvalue of the return type after the cv modifier.

  • In initialization, the initializer is the same type of prvalue as the variable.

It is worth noting that this type of behavior is not an optimization in C++17 because there are no temporary objects to copy or move.In fact, C++17 redefines the expression category:

  • The evaluation of glvalue can determine the identity of objects, bit domains and functions.

  • The evaluation of prvalue initializes the object or bit field, or calculates the value of an operand, depending on the context;

  • xvalue is a glvalue that indicates that a resource of an object or bit field can be reused.

  • Is lvalue the glvalue of xvalue?

  • rvalue is prvalue or xvalue.

This definition is functionally the same as in C++11, but it more clearly points out the difference between glvalue and prvalue - glvalue produces addresses and prvalue performs initialization.

The object prvalue initializes is determined by the context: in the case of copy omission, prvalue has no associated object; in other cases, prvalue produces a temporary object, a process known as temporary materialization.

Temporary materialization converts a full-type prvalue to xvalue, which occurs in the following situations:

  • Bind references to prvalue;

  • The class prvalue is obtained as a member;

  • The array prvalue is converted to a pointer or subscript element;

  • prvalue appears in the brace initialization list to initialize an std::initializer_List<T>;

  • Is used with the typeid or sizeof operator;

  • In the statement expr; or converted to void, the value of the expression is discarded.

Or it can be understood that prvalue is temporarily materialized in all non-copy omissions.

class NonMoveable
{
public:
    int i = 1;
    NonMoveable(int i) : i(i) { }
    NonMoveable(NonMoveable&&) = delete;
};

NonMoveable make(int i)
{
    return NonMoveable{i};
}

void take(NonMoveable nm)
{
    return static_cast<void>(nm);
}

int main()
{
    auto nm = make(2);
    auto nm2 = NonMoveable{make(3)};
    // take(nm);
    take(make(4));
    take(NonMoveable{make(5)});
}

The NonMoveable's move constructor is declared delete, so the copy constructor is also implicitly deleted.In auto nm = make(2); NonMoveable{i} is prvalue, which is constructed directly as a return value according to the first rule omitted from the copy; the return value is the prvalue of NonMoveable, which is the same as the type of nm, and according to the second rule, this prvalue is constructed directly at the position of the nm; in combination, the declaration corresponds to NonMoveable nm{2};.

In MSVC, this piece of code cannot be compiled because the compiler fails to strictly adhere to the C++ standard.However, if an output statement is added to the NonMoveable's mobile constructor, the program will run without any output, even in Debug mode, even when compiled using the C++11 standard.This also reflects the significance of copy omission.

summary

C++11 specifies that each expression belongs to one of the three categories lvalue, xvalue, and prvalue. Expressions can be divided into lvalue and rvalue, or glvalue and prvalue.The function call that returns the right value reference is xvalue, and the variable of the right value reference type is lvalue.

const left-value and right-value references can bind temporary objects, but the declaration cycle of temporary objects can only be extended once, and returning a right-value reference pointing to a local variable can also cause an empty reference.

Identifiers plus a pair of parentheses become expressions, and decltype is used for expressions to produce corresponding types based on their category. Variables can be declared with decltype(auto) to preserve the expression category.

In C++17, whether prvalue has an associated object is determined by the context. Copy omission specifies that objects are constructed directly without copying or moving in a particular case. NRVO becomes a mandatory standard, making it semantically transferable for objects that cannot be moved.

Reference resources

Tags: C++ Mobile Lambda less REST

Posted on Sat, 23 May 2020 12:44:44 -0400 by luzlin