Common features of C++ 11

Posted by rkeppert on Sun, 26 Sep 2021 18:38:24 +0200

12.Lambda expression

Lambda expression is one of the most important and commonly used features of C++11. It is a feature of modern programming languages. Lambda expression has a declarative programming style. 2. Simplicity, avoiding code expansion and function dispersion. 3. Features such as closure of functions in the time and place required.

//Grammar Format:
[capture](params)opt -> ret{body;};

//captrue is a variable whose capture list captures a range of variables

//params is a parameter list like a function parameter list, no parameters can be omitted from writing
auto f1 = [](){return 1;}; //No parameters, parameter list is empty
auto f2 = []{return 1;}; //No parameters, parameter list omitted

//opt is a function option and does not need to be omitted
mutable:Copies passed in by value can be modified(Note that the copy can be modified, not the value itself)
exception: An exception thrown by a specified function, such as an integer type, can be used throw ();
    
//ret is the return value type
 stay C++11 Medium, lambda The return value of an expression is defined by the return value postposition syntax
    
//Body is a function body
  • Capture List

    Capture methods can be divided into the following categories:

    • [] - Do not capture any variables
    • [&] - Captures all variables in the external scope and uses them as references in the function body
    • [=] - Captures all variables in the external scope and ==uses as a copy (value)==in the body of the function
    • [=, &var] - Capture all external variables by value and by reference var
    • [var] - Capture var variables by value
    • [&var] - Capture var variables by reference
    • [this] - Capture this pointer in the current class
  • Return type

Many times, the return value of a lambda expression is very obvious, so omitting the return value of a lambda expression is allowed in C++11.

//Complete lambda expression
auto f1 = [](int a)-> int
{
    return a+10;
}

//Ignore return value writing
auto f2 = [](int a)
{
    return a+10;
}

Normally, without specifying the return value of a lambda expression, the compiler will automatically deduce the return type based on the return statement, but it is important to note that labmda expressions cannot automatically deduce the return type through list initialization.

// ok, the return type can be derived automatically
auto f = [](int i)
{
    return i;
}

// error, cannot deduce return value type
auto f1 = []()
{
    return {1, 2};	// Derive return value based on list initialization, error
}
  • Functional Essence

Use lambda expression capture list to capture external variables, if you want to modify the external variables captured by value, what should you do? This requires the mutable option, which is a lambda expression that specifies the list of parameters even if there are no parameters, and can remove the read-only (const) property of the external variables captured by value.

int a = 0;
auto f1 = [=](){return a++;}; //error a is read-only
auto f2 = [=]()mutable{return a++;}; //ok

1. The type of lambda expression is considered to be a class with operator(), i.e., a pseudofunction, in C++11.

2. The operator() of a lambda expression is const by default, and a const member function cannot modify the value of a member variable. The mutable option cancels the const attribute of the operator().

Because lambda expressions are treated as a function-like in C++, they can be stored and bound using std::function and std::bind:

//Packaging dispatchable functions
std::function<int(int)> f1 = [](int a){return a;};
//Binding schedulable functions
std::function<int(int)> f2 = bind([](int a){return a;},placeholders::_1);

//call
f1(10);
f2(20);

A lambda expression that does not capture any variables can also be converted to a normal function pointer.

using func_ptr = void(*)(int);
func_ptr f = [](int a){cout<<a<<endl;};
//call
f(100);

13.rvalue reference

C++11 adds a new type, called R-value reference, labeled &&. First let's look at the right and left values:

  • lvalue is the abbreviation of locator value and rvalue is the abbreviation of read value
  • The left value refers to data stored in memory with an explicit storage address (preferable address).
  • Right value refers to data that can provide data values (unavailable addresses)
int a = 10; // a is left and 10 is right

There are two kinds of right values in C++: one is dead value, the other is pure right value.

  • The expression associated with the reference to the right value, such as the return value of a T&type function, the return value of std::move, etc.
  • Pure right value: Temporary variable returned by non-reference, temporary variable generated by operation expression, original literal quantity, lambda expression, etc.

A right-value reference is the type of reference to a right-value. Because the right-value is anonymous, we can only find it by reference. Whether you declare a left-value reference or a right-value reference, it must be initialized immediately, because the reference type itself does not own memory for the bound object, it is just an alias for the object. By a declaration of a right-value reference, the right-value is"Rebirth" has the same life cycle as the right-value reference type variable.

class rval{
public:
	rval(){cout<<"rval construct"<<endl;}

	~rval(){cout<<"rval desconstruct"<<endl;}
};

rval getRval(){
	return rval();
}

void rValue(){
	int a1; //Left Value
	int&& a2 = a1; //error left value cannot initialize right value reference
	rval&& r1 = getRval(); //The temporary variable returned by ok getRval() is called the dead value, and r1 is a reference to the right value of the dead value
    
    rval& r2 = getRval(); //error rval() is an anonymous object, the right value, and cannot be initialized for a normal left-value reference
	const rval&& r2 = getRval(); //The ok constant left-value reference is a universal reference type that accepts left, right, left, and right constants.
}
  • performance optimization

When assigning objects in C++, deep copies between objects occur in many cases. If the heap memory is large, the cost of this copy can be very high. In some cases, if you want to avoid deep copies of objects, you can use right-value references to optimize performance.

 // Right Value Reference Optimization
class rval2 {
public:
	rval2() :ptr(new int(100)) { cout << "Construct" << endl; }
	rval2(const rval2& ref) :ptr(new int(*ref.ptr)) { cout << "Copy Construct" << endl; }

	~rval2() { 
        delete ptr; 
        cout << "Destruct" << endl;}

	int* ptr;
};

rval2 getRef() {
	rval2 r;
	return r;
}


void rValue2() {
	rval2 r = getRef();
}

Program results:

construct
Destruct
100

From the output, you can see that rval2 r = getRef() is called; when a copy constructor is called, a deep copy of the returned temporary object is made to get the object t, in getRef()An object created in a function is freed without being used, although it performs a memory request operation. If the resources already requested by the temporary object can be used, it will save both resources and time for resource request and release. If you want to perform such an operation, you need to use a right value reference, which has mobile semantics and mobile semantics can make resources available.(Heap, System Objects, etc.) Transferring shallow copies from one object to another reduces the creation, copy, and destruction of unnecessary temporary objects, and greatly improves the performance of C++ applications.

// Right Value Reference Optimization
class rval2 {
public:
	rval2() :ptr(new int(100)) { cout << "Construct" << endl; }
	rval2(const rval2& ref) :ptr(new int(*ref.ptr)) { cout << "Copy Construct" << endl; }

	//Move a constructor to transfer objects in a shallow copy 
	rval2(rval2&& ref) :ptr(ref.ptr) {
		ref.ptr = NULL;
		cout << "Move Construct" << endl;
	}

	~rval2() { delete ptr; cout << "Destruct" << endl;
	}

	int* ptr;
};

rval2 getRef() {
	rval2 r;
	return r;
}


void rValue2() {
	rval2 r = getRef(); // take
	cout << *r.ptr << endl;
}

The test results are as follows:

Construct
Move Construct
Destruct
100
Destruct

Modification adds a move constructor (parameter is right-value reference type) to the rval2 class above. Instead of invoking a copy constructor for a deep copy, the move constructor is invoked for a shallow copy of this function, no deep copy of the temporary object, which improves performance.

If no mobile construct is used, a shallow copy is also made when rval2 r = getRef(), but when the temporary object is destructed, the class member pointer int*ptr;The pointed memory is also destructed.

In the test program, the return value of getRef() is a dead value, that is, a right value. If = right is a right value in the assignment operation, then the move construct is called.

--Summary: For classes that require a large number of resources, mobile constructors should be designed to improve program efficiency. It is important to note that we generally provide a mobile constructor along with a copy constructor that is applied to a constant left value to ensure that the move is not successful or that a copy constructor can be used.
  • &Features of

In C++, not all cases && represents a right-value reference. The specific scenario is reflected in the template and automatic type derivation. If the template parameter needs to be specified as T &&, the automatic type derivation needs to be specified as auto &&. In both cases && is called an indeterminate reference type, that is, a left-value reference or a right-value reference.

Another point to note is that const T&amp means a right-value reference, not an indeterminate reference type.

Start with the first example, using && in a function template.

template<typename T>
void func1(T&& param);
void func2(const T&& param);

f1(10); //10 is the right value, where T&is the right value reference 
int x = 10;
f1(x); //x is the left value, where T&is the left value reference 
f2(x); //Const T&is not an indeterminate reference type, does not require derivation, and represents a right-value reference itself

Consider the second example.

int x = 10, y = 20;
auto && v1 = x; //Auto & & represents a left-value reference to an integer
auto && v2 = 30; //Auto & & represents a right-value reference to an integer

decltype(x+y)&& v3 = y; //decltype is equivalent to int & & is a right value reference, y is a left value, and cannot initialize a right value reference type with a left value

Because of the indeterminate reference type T&or auto&in the above code, when it is used as a parameter, it may be initialized by a right-value reference or by a left-value reference, and the right-value reference type (&&) will change during type derivation, which is called reference folding. The rules for reference folding in C++11 are as follows:

  • Deriving T&or auto&from right values yields a right value reference type
  • Deriving T&or auto&from a non-right value yields a left value reference type (right value reference, left value reference, left value reference, right value reference of a constant, left value reference of a constant)
// Right Value Type Derivation
void rvalueType() {
	const int&& x1 = 5;
	const int& x2 = x1;
	int&& x3 = 6;
	int& x4 = x3;

	auto&& a = 10; //10 is right value, Auto & & deduced as right value reference
	auto&& b = x1; //x1 is a constant right-value reference, Auto & & deduced as a left-value reference
	auto&& c = x2; //x2 is a constant left-value reference, Auto & & deduced as a left-value reference
	auto&& d = x3; //x3 is a right value reference, Auto & & deduced as a left value reference
	auto&& e = x4; //x2 is a left-value reference, Auto & & deduced as a left-value reference
}

Let's look at the last example:

#include <iostream>
using namespace std;

void printValue(int &i)
{
    cout << "l-value: " << i << endl;
}

void printValue(int &&i)
{
    cout << "r-value: " << i << endl;
}

void forward(int &&k)
{
    printValue(k); //The right value passed in is assigned to the right value reference k, but when the function printValue() is called in this function, the parameter K becomes a named object and the editor treats it as a left value.
}

int main()
{
    int i = 520;
    printValue(i); //Call Left Value
    printValue(1314); //Call Right Value
    forward(250);

    return 0;
};

From the code above, you can conclude that the editor treats named right-value references as left-values and unnamed right-values as right-values.

14.Transfer and Perfect Forwarding

  • move

A right-value reference was added in C++11, and the right-value reference cannot be initialized with a left value. If you want to initialize a right-value reference with a left value, you need std::move() Function that converts left values to right values using the std::move method. This function does not move anything, but has the same mobility semantics as the move constructor, transferring the state or ownership of an object from one object to another, only without a memory copy.

Implementing std::move is equivalent to a type conversion static_cast <T&> (lvalue);The function prototype is as follows:

template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{	// forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}

Use it as follows:

//move
void movefunc(int&& x) {
	cout << x << endl;
}


void moveTest() {
	int x = 10;
	//Movefunc(x);Error passed in a left value
	movefunc(move(x)); //Modify left x to right using move
}

Assuming that a temporary container is large and needs to be assigned to another container, you can do the following:

list<string> ls;
ls.push_back("unreal");
ls.push_back("unity");
...

list<string> ls1 = ls; //Need to copy, inefficient
list<string> ls1 = move(ls);

If you use copies, the cost is enormous and inefficient. There is little or no cost to using move, but the ownership of the resource is transferred. If there is a large heap memory or a dynamic array inside an object, using move() is a convenient way to transfer ownership of the data. In addition, we can write appropriate mobile constructors for classes (T::T(T&Another))And assignment functions with moving semantics (T&T::operator=(T&rhs)) reuse resources as much as possible when constructing objects and assignments, since they both receive a right-value reference parameter.

  • forward

The right-value reference type is value-independent. When a right-value reference is a parameter of a function, it becomes a left-value when the parameter is forwarded to other functions inside the function. It is not the original type. If you need to forward to another function according to the original type of parameter, you can use std::forward() provided by C++11.Function, which implements a function called perfect forwarding.

// Function Prototype
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// What it looks like after streamlining
std::forward<T>(t);
  • When T is a left-value reference type, T will be converted to a left-value of type T

  • When T is not a left value reference type, T will be converted to the right value of T type

    Here is an example of the use of forward:

//forward
template<typename T>
void printValue(T& t) {
	cout << "l-value:" << t << endl;
}

template<typename T>
void printValue(T&& t) {
	cout << "r-value:" << t << endl;
}


template<typename T>
void forwardTest(T&& t) {
	printValue(t);
	printValue(move(t));

	printValue(forward<T>(t));
	cout << endl;
}



void forwardfunc() {
	forwardTest(520); // l r r
	int num = 50;

	forwardTest(num); //l r l
	forwardTest(forward<int>(num)); //l r r
	forwardTest(forward<int&>(num)); //l r l
	forwardTest(forward<int&&>(num)); //l r r
}

The test code prints as follows:

l-value:520
r-value:520
r-value:520

l-value:50
r-value:50
l-value:50

l-value:50
r-value:50
r-value:50

l-value:50
r-value:50
l-value:50

l-value:50
r-value:50
r-value:50

Topics: C++