您现在的位置是:首页 >其他 >现代C++技术研究(7)---std::move的使用场景网站首页其他

现代C++技术研究(7)---std::move的使用场景

swordmanwk 2024-06-17 11:26:54
简介现代C++技术研究(7)---std::move的使用场景

支持移动语义,是现代C++的主要语言特性之一,std::move本质上就是把一个变量强转成右值引用。在gcc的源码中,std::move的实现如下:

  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

通常,我们在程序中使用std::move都是为了触发移动构造函数或移动赋值运算符的调用,举个例子:

#include <string>

int main()
{
    std::string str = "123";
    std::string str2 = std::move(str);
    return 0;
}

显然,通过std::move触发了移动构造函数。

如果等号的右侧是一个临时对象,则不需要调用std::move,这种场景,编译器会自动触发移动操作:

#include <string>

int main()
{
    std::string str;
    str = std::string("123");
    return 0;
}

通过对应的汇编代码,我们确认,调用了std::string的移动赋值运算符:

.LC0:
        .string "123"
main:
        stp     x29, x30, [sp, -112]!
        mov     x29, sp
        str     x19, [sp, 16]
        add     x0, sp, 32
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string() [complete object constructor]
        add     x0, sp, 96
        str     x0, [sp, 104]
        nop
        nop
        add     x0, sp, 96
        add     x3, sp, 64
        mov     x2, x0
        adrp    x0, .LC0
        add     x1, x0, :lo12:.LC0
        mov     x0, x3
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&) [complete object constructor]
        add     x1, sp, 64
        add     x0, sp, 32
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator=(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&)

原因其实也容易理解,临时对象就是右值,编译器对于右值的处理方式,就是调用移动构造函数或者移动赋值运算符。

如果一个函数的传入参数是常量,则调用std::move将无法触发移动构造:

#include <string>

int main()
{
    const std::string str = "123";
    std::string str2 = std::move(str);
    return 0;
}

这种场景下,编译器生成代码实际上调用的是std::string的拷贝构造函数:

.LC0:
        .string "123"
main:
        stp     x29, x30, [sp, -112]!
        mov     x29, sp
        str     x19, [sp, 16]
        add     x0, sp, 96
        str     x0, [sp, 104]
        nop
        nop
        add     x0, sp, 96
        add     x3, sp, 64
        mov     x2, x0
        adrp    x0, .LC0
        add     x1, x0, :lo12:.LC0
        mov     x0, x3
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&) [complete object constructor]
        add     x0, sp, 96
        bl      std::__new_allocator<char>::~__new_allocator() [base object destructor]
        nop
        add     x0, sp, 64
        bl      std::remove_reference<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>::type&& std::move<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
        mov     x1, x0
        add     x0, sp, 32
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) [complete object constructor]
        mov     w19, 0
        add     x0, sp, 32
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        add     x0, sp, 64
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     w0, w19
        b       .L7
        mov     x19, x0
        add     x0, sp, 96
        bl      std::__new_allocator<char>::~__new_allocator() [base object destructor]
        nop
        mov     x0, x19
        bl      _Unwind_Resume
        mov     x19, x0
        add     x0, sp, 64
        bl      std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
        mov     x0, x19
        bl      _Unwind_Resume
.L7:
        ldr     x19, [sp, 16]
        ldp     x29, x30, [sp], 112
        ret
DW.ref.__gxx_personality_v0:
        .xword  __gxx_personality_v0

这个也容易理解,移动构造函数可能会修改入参,因为定义为常量,不能修改,所以不能调用移动构造函数,但是拷贝构造函数的入参是可以接受常量的。实际上std::move的类型强转是成功的,转型的结果就是常量右值引用:

#include <string>
#include <iostream>

template<typename T> void f(T&& param) // universal reference
{
    using ParamType = T&&;
    bool isCnstRValRef = std::is_same<ParamType, const std::string&&>::value;
    if (isCnstRValRef) {
        std::cout << "param's type is const std::string&&
";
    } else {
        std::cout << "param's type is other type
";
    }
}

int main()
{
    const std::string str = "123";
    f(std::move(str));
    std::string str2 = std::move(str);
    return 0;
}

测试结果就是const std::string&&:

[root@192 moderncpp]# ./test_move
param's type is const std::string&&

总结一下:

使用std::move就是为了把入参强转为右值,通常这样做是为了触发移动构造函数的调用,大部分场景都是OK的,但是,如果入参是常量,不要加std::move,因为加了也不会调用移动构造函数;如果入参是临时对象,也不要加std::move,因为不加也会调用移动构造函数。

参考资料:

《Effective Modern C++》

《C++并发编程实战》

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。