usage of reference in template(reading notes of Meyer'book)

本文最后更新于:August 1, 2020 pm

usage of reference in template

In my opinion,universal reference,reference,move semantics and the perfect forward are the essence of modern cpp.After i chewing over Meyer’s book and C++ primer,i think i can figure out it. So i want to share my experience in this blog.

the deduction for parameter and the template

ParamType is a Reference or Pointer, but not a Universal Reference.

for example:


template < typename T >
auto f(T& parameter){
    //some code
}

For this situation you just need to discard the reference or pointer part and remaining type is the type of T,especially,when the parameter type is const the type for T is no need to be a const any more.Besides,when it comes to low level const for pointers,you can ignore it.

We use the term top-level const to indicate that the pointer itself is a const. When a pointer can point to a const object, we refer to that const as a low-level const.

auto f(const T* parameter)
auto f2(T*parameter)
const int a=1;
int b=2;
int*p1 = &b;
auto *p2=&a;// when you use auto to deduce the type,the top level const is ignored,and the low level const(if have) is remained 
f(p1) // T:int,param:const int*
f(p2) // T int,param:const int*
f(p2) // T const int,param:const int*

parameter is an universal reference

a universal reference not only occurred in the template but also used in auto.Since the auto rules is similar to the template one
a universal reference must have direct type deduction and a format of T&& or arges&&...arge

for example void f(T&&a) is an universal reference.In this case,wo first need to determine the type of T,and apply the rule of reference collapsing to it.and if the passed in parameter is lvalue,than T is a lvalue reference,and the type of parameter is lvalue reference,and if the passed in parameter is rvalue,you can refer to case 1.It wll become to an normal rvalue reference.

the rule of reference collapsing is used when there is a reference to a reference.The rules suggests that only both two reference are rvalue reference,you will get a rvalue reference.Otherwise,you get a lvalue reference.

parameter is neither a pointer nor a reference(pure value)

void f(T para)There are two rules to deduce to type of T

  • if there is a reference,ignore it.
  • if there is a const or a violate,ignore it.(because when you call a function by value,it will copy a new variable,and the modification of the new variable will not alter the original one.Thus,you don’t need a const any more)

parameter is an array

  • when the function is auto f(T para) the array will decay in to a pointer and such syntax is in line with those in C.Since,the C programmer thinks that the expenses of copying an array into the function is very high.
  • when the para type of the function is a reference,either lvalue or rvalue,para will become a reference to an array and the form of the type is similar to (element type)&[element number] such as int&[12]

a ingenious code which determine the size of the array at the compile time

template < typename T,std:size_t N >
constexpr std::size_t arraysize(T(&)[N])noexcept{
    return N
}

move and forward

First,let me introduce the type traits(from meyer’s book item 9), it is an assortment of templates inside the header <type_traits>. There are dozens of type traits in that header, and not all of them perform type transformations, but the ones that do offer a predictable interface. Given a type T to which you’d like to apply a transformation, the resulting type is std::transformation<T>::type. For example:

std::remove_const < T >::type // yields T from const T
std::remove_reference < T >::type // yields T from T& and T&&
std::add_lvalue_reference < T >::type // yields T& from T

After C++14 you can use remove_const_t<T> to instead the above-mentioned
these two functions are merely used to cast type.std::move unconditionally casts its argument to an rvalue, while
std::forward performs this cast only if a particular condition is fulfilled. That’s it.

std::move

the source code of move

template < typename T > // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = // alias declaration;
typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}

you can use remove_reference_t<T> to replace remove_reference<T>::type and use auto to declare the return type after C++ 14 so the code after C++14 will be as followed

template < typename T > // C++14; still in
decltype(auto) move(T&& param) // namespace std
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

when you use auto to deduce the return type,it will adopt the rules of template deduction,which will ignore the reference so you should use decltype(auto) to instead

There are two critical notes for using the std::move

  • don’t declare objects const if you want to be able to move from them. Move requests on const objects are silently transformed into copy operations.
  • don’t declare objects const if you want to be able to move from them. Move requests on const objects are silently transformed into copy operations.

because the std::move only return an rvalue and it doesn’t affect the constness of the variable,so after you use std::move to move a variable,a move constructor or a move operator = should be called to turn over the ownership of the object.But since it is a const,the type of parameter doesn’t fit in,so the compiler have to use the copy assignment since its type is const type&,and ir accept the rvalue,even the const rvalue.

std:forward

std::forward is a conditional cast: it casts to an rvalue only if its argument was initialized with an rvalue.

std::forward are always used for decorator to pass the true parameter,when you need a unconditional rvalue,use move.

template < class T >
constexpr T&& forward(std::remove_reference_t< T > & arg) noexcept{
    // forward an lvalue as an lvalue
    return (static_cast<T&&>(arg));
}

template < class T >
constexpr T&& forward(std::remove_reference_t< T > && arg) noexcept{
    // forward an rvalue as an rvalue
    return (static_cast<T&&>(arg));
}

forward and universal reference

It will be dangerous to simply use move in the universal reference because when we want to retain the ownership of the original variable and we passed a non-const reference to it,then the original variable will be in an undefined state.for example:

    class test
    {
    public:
        template < typename T >
        test(T&&a):name(move(a)){}
    private:
        string name;
    };
    int main() {
        string s = "123";
        test a(s);
        cout << s;
}

in this example we can figure out that don’t use move in the rvalue reference,use forward.
Moreover,use the overloaded functions which take a lvalue reference and a rvalue reference as the parameter will lose efficiency because the call of constructor and destructor
an ingenious code which can only be implemented by universal reference

template < class...  Args >
void forward(Args&&... args) {
    f(std::forward<Args>(args)...);
}

you can’t overload the function which takes several universal references

template < class T, class... Args > // from C++11
shared_ptr< T > make_shared(Args&&... args); // Standard
template < class T, class... Args> // from C++14
unique_ptr< T > make_unique(Args&&... args); // Standard

so we comes to the conclusion:
std::forward is applied to the universal reference parameters when they’re passed to other functions. Which is exactly what you should do

copy Elision and RVO

in some cases,the compiler will automatically help us to optimize the return value,which means the return value and lhs will share the same address.the so if we called a move to the return value,it will call a move constructor which will affect the efficiency.so we will summarize some scenario that need the programmer to apply move to the return value.

URVO (Unnamed Return Value Optimization)

it means all the return value has the same type and the type is identified with the declaration of the function
for example:

// they have the same 'user' type
User create_user(const std::string &username, const std::string &password) {
    if (find(username)) return get_user(username);
    else if (validate(username) == false) return create_invalid_user();
    else return User{username, password};
}

NRVO(Named Return Value Optimization)

it means return the same local variable every time.

User create_user(const std::string &username, const std::string &password) {
    User user{username, password};
    if (find(username)) {
        user = get_user(username);
        return user;
    } else if (user.is_valid() == false) {
        user = create_invalid_user();
        return user;
    } else {
        return user;
    }
}

situations you need to apply move

the most important one is that the return type is a reference rather than a value.Either universal reference or rvalue reference for example

template< typename T >
Fraction // by-value return
reduceAndCopy(T&& frac) // universal reference param
{
frac.reduce();
return std::forward< T >(frac); // move rvalue into return
} // value, copy lvalue

or rvalue reference

Matrix // by-value return
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // move lhs into
} // return value

So all in all Never apply std::move or std::forward to local objects if they would otherwise be eligible for the return value optimization.But you should use std::move to the reference type


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!