打开导航 打开导航 Open menu
量化交易系统开发(C++)/ 期权策略与风控 / 行业研究分析
量化交易系统開發(C++)/ 期权策略与风控 / 行业研究分析
C++ trading systems / options strategy & risk controls / equity research
adrian@adrianxv.cn
文章目录

Value Categories, References & noexcept Value Categories, References & noexcept Value Categories, References & noexcept

lvalue, prvalue, xvalue

We have to distinguish between types and value categories.

We’ll first talk about types:

  1. value type: int
  2. lvalue reference type: int&
  3. const lvalue reference type: const int&
  4. rvalue reference type: int&&
  5. const rvlaue reference type: const int&&

then value categories:

  1. lvalue
  2. xvalue (eXpiring value): an object whose resources can be moved from
  3. prvalue

Starting from the declared variables of the 5 types, which are all lvalue, we can derive xvalue and prvalue from them:

Variable’s declared typelvalue expressionxvalue expressionprvalue expression
intxstd::move(x)x + 1
int&astd::move(a)a + 1
const int&cstd::move(c)c + 1
int&&bstd::move(b)b + 1
const int&&dstd::move(d)d + 1

In terms of binding, a value type creates a new object; lvalue refernece type and rvalue reference types has to bind to an object.

#include <utility> // std::move

int main() {
    int x = 10;

    // ==========================================
    // 1. LVALUE EXPRESSIONS (x)
    // ==========================================

    int a1 = x;             // ✅ Valid
    int& a2 = x;            // ✅ Valid
    const int& a3 = x;      // ✅ Valid
    // int&& a4 = x;        // ❌ Invalid
    // const int&& a5 = x;  // ❌ Invalid


    // ==========================================
    // 2. XVALUE EXPRESSIONS (std::move(x))
    // ==========================================

    int b1 = std::move(x);             // ✅ Valid
    // int& b2 = std::move(x);         // ❌ Invalid
    const int& b3 = std::move(x);      // ✅ Valid
    int&& b4 = std::move(x);           // ✅ Valid
    const int&& b5 = std::move(x);     // ✅ Valid


    // ==========================================
    // 3. PRVALUE EXPRESSIONS (x + 1)
    // ==========================================

    int c1 = x + 1;          // ✅ Valid
    // int& c2 = x + 1;      // ❌ Invalid
    const int& c3 = x + 1;   // ✅ Valid
    int&& c4 = x + 1;        // ✅ Valid
    const int&& c5 = x + 1;  // ✅ Valid
}

std::move

std::move doesn’t move anything, and generate no code to run at runtime. It simply casts whatever value category to an xvalue category. So, honestly, std::move is just a wrapper around static_cast, and xvalue_cast would be a better name for std::move. The xvalue is a candidate for a move operation.

You cannot move from a const object. The move operation would transform into a copy operation instead.

std::string a = "a";
const string b = "b";

std::string a1 = std::move(a);  // move operation
std::string b1 = std::move(b);  // copy operation

std::move(a) is an xvalue of type std::string&&, which is going to invoke the move constructor; whereas std::move(b) is an xvalue of type const std::string&&, with a const, which is not going to invoke the move constructor, but the copy constructor instead.

std::forward

std::forward<T>(x) is also a wrapper around static_cast. It casts x into type lvalue reference (T&) or rvalue reference (T&&) based on whether T is lvalue reference type (e.g. int&) or plain value type (e.g. int). And the value category of std::forward<T>(x) as an expression is also evaluated differently as lvalue or rvalue. You must decide if you want to get an rvalue reference or an lvalue reference by specifying T explicitly using <>.

The simplified implementation for std::forward is:

template<typename T>
constexpr T&& std::forward(std::remove_reference_t<T>& x) noexcept {
    return static_cast<T&&>(x);
}

Here, the std::remove_reference_t<T> is a type, not a function. So std::remove_reference_t<T>& x removes any reference type from x and adds back a & to make sure that x inside the function body is lvalue reference whatever type we pass in. We used std::remove_reference_t<T>& x instead of plain T& x because we want to disable automatic template deduction for type T.

Here T must be explicitly specified by the caller using <T>, e.g. . In static_cast, reference collapsing happens: if T is an lvalue reference, T&& is also an lvalue reference, thus x is cast into an lvalue reference; If T is a plain value type, then T is an rvalue reference, thus x is cast into an rvalue reference.

In the most common use case of std::forward, which is perfect forwarding, we first use template type deduction. It follows the rule that “If the argument is an lvalue expression, deduce T as lvalue reference. If the argument is a rvalue expression, deduce T as value type”. Thus if we pass in wrapper a named int x, T is int&; if we pass in wrapper std::move(x), T is int.

// most common use case
template<typename T>
void wrapper(T&& arg) {
    foo(std::forward<T>(arg));
}

And this works perfectly with std::forward<T>, because passing in T as int& would derive a int&; passing in int would derive int&&. And the value category of the expression std::forward<T>(arg) is evaluated to be lvalue if the return type is int&, and rvalue if the return type is int&&. This is exactly our goal (!!!!!!): preserving the original argument’s value category (lvalue vs. rvalue) when forwarding it to foo.

You can pass into std::forward<T>(x) an x of all value categories, that is, lvalue, xvalue and prvalue.

int x = 5;

std::forward<int>(x);             // valid: returns int&& as xvalue
std::forward<int>(std::move(x));  // valid: returns int&& as xvalue
std::forward<int>(x + 1);         // valid: returns int&& as xvalue

std::forward<int&>(x);            // returns int& as lvalue
std::forward<int&>(std::move(x)); // returns int& as lvalue
std::forward<int&>(x + 1);        // ERROR: cannot bind prvalue to lvalue reference

Universal Reference

Universal reference (AKA Forwarding reference) is nothing more than “T&& or auto&&, where the type can be deduced on the spot, followed by reference collapsing”. It is called universal reference because it can bind to both lvalues and rvalues. The actual type of T&& or auto&& is deduced to be a rvalue reference if you pass in an rvalue (prvalue or xvalue); deduced to be a lvalue reference if you pass in an lvalue.

Let’s look at 2 examples that fit this definition (arg and x):

template<typename T>
void foo(T&& arg);

auto&& x = y;

Here, foo can deduce the type of T based on the type of arg, but you can also explicitly declare the type of T using <> like foo<int>(1). Regardless, then T&& is deduced to T& or T&& depending if you pass in lvalue or rvalue. Same with auto&&, which is deduced to lvalue reference or rvalue reference depending if you pass in lvalue or rvalue. So here we reinforce the point that there is basically no difference between template type deduction and auto type deduction except for some nuances. (Check out my blog about auto )

Let’s walk through the foo example. If you do

int x = 5;
int& y = x;

foo(x);
foo(y);
foo(std::move(x));

First, there is a template argument deduction, which follows the rule that “If the argument is an lvalue expression, deduce T as lvalue reference. If the argument is a rvalue expression, deduce T as value type”. So either x or y passed into foo results in T being deduced to int&; whereas std::move(x) passed into foo results in T being deduced to int.

Then comes the reference collapsing rule when the compiler decides what T&& is: int& collapses into int& (nothing changes); int collapses into int&&. So by passing in lvalue (x or y), you end up getting a lvalue reference; by passing in rvalue (std::move(x)), you end up getting a rvalue reference.

So what do we do with int& and int&& that we get? We either perfectly forward it to some other function (as we discussed before using std::forward), or we judge what reference it is and process it accordingly:

template<typename T>
void foo(T&& arg) {
    if (std::is_rvalue_reference_v<T>) {
        // called with lvalue
    }
    else {
        // called with rvalue
    }
}

Now let’s look at what is NOT universal reference. In our previous implementation for std::forward, the compiler cannot deduce the type for T and the caller must explicitly state the type when calling. e.g. std::forward<int>(x). Plus the T&& appears in the return type, where no deduction happens on the spot. So here T&& is not a universal reference.

template<typename T>
constexpr T&& std::forward(std::remove_reference_t<T>& x) noexcept {
    return static_cast<T&&>(x);
}

This is also not a universal reference, because the deduced type is for elements inside of the vector, not the vector itself:

template<typename T>
voif foo(std::vector<T>&& v);

Reference Collapsing

Reference collapsing refers to the rule that if we stack reference types, the type collapses to either lvalue reference or rvalue reference. The rule works as:

  • T& & collapses to T&
  • T&& & collapses to T&
  • T& && collapses to T&
  • T&& && collapses to T&&

So you get T& most of the time, except for when you stack two rvalue references.

And note that you cannot directly stack references in variable declaration (int & && x gets an error). Reference collapsing is only valid during template instatiation, auto type generation, typedefs/alias, decltype. I’ll only illustrate the last 2 here since the first 2 is seen in universal reference:

using L = int&;
using R = int&&;

using A = L&&;  // int&
using B = R&&;  // int&&

int x = 5;
int& y =x;
decltype(y)&& z = x; // int&

constexpr functions

constexpr functions can either be evaluated at compile time or runtime. It depends on the optimization level.

If the value of a constexpr function is requested during compile time constexpr variables, it will be executed at compile time.

noexcept

A function is declared as noexcept if you don’t want it to throw any exceptions. Specifically, this often indicates the following scenario:

  • The program had better crash if an exception is thrown inside the functions.
  • The function does not support exceptions, e.g. functions in C (the C++ Standard Library implicitly adds noexcept to all C Standard Library functions)

Thus, a noexcept function shouldn’t call any function that expects it to handle exceptions, unless you explicitly hopes the program to std::terminate (there is no point in keeping the program running).

THe six generated special member functions are all implicitly noexcept.