0
点赞
收藏
分享

微信扫一扫

YOLOv2 (You Only Look Once Version 2)

水沐由之 2024-12-04 阅读 7
c++面试

移动语义std::move

C++11新增了的特性,移动语义可以在处理大型数据结构或资源密集型对象时避免深度拷贝,从而显著提升性能。

引入:拷贝 vs 移动

C++可以从一个已有对象构造一个新对象称为拷贝构造,当该类型中含有指针指向堆上分配的资源时,拷贝构造函数需要进行深拷贝,即在堆上分配新的空间,将指针指向的内容也进行拷贝。
对于函数调用等情况,会产生临时变量,或者生命周期到头的变量,例如:

#include<iostream>
class A
{
public:
        A(int capacity = 0):_capacity(capacity){
            _data = (int*)malloc(sizeof(int) * capacity);
        }
        int capacity() const{
            return _capacity;
        }
        ~A(){
            if(_data != nullptr){
                free(_data);
            }
        }
private:
    int _capacity = 0;
    int* _data = nullptr;
};

class B{
    public:
        B(int capacity) : _capacity(capacity){
            _data = (float*)malloc(capacity*sizeof(float));
        }
        ~B(){
            if(_data != nullptr){
                free(_data);
            }
        }
    private:
        int _capacity = 0;
        float* _data = nullptr;
};

B test(A a){
    std::cout << "Test" << std::endl;
    B ans(a.capacity());
    return ans;
}

int main(){
    A a(10);
    B temp = test(a);
    return 0;
}

当值传递的时候,会调用拷贝构造函数构造一个新的对象传给函数,但是如果这个对象,在传给这个函数后,就不需要了,那我们是否可以直接将这个对象就传给函数,而不调用拷贝构造,这样是否会快一些?还有在函数中生成的B对象,在返回的时候是否可以直接用这个B对象,而不是再拷贝构造一个。
答案就是今天的主角,移动语义,顾名思义,就是将

前置知识:左值右值

C++中左值右值问题
在这里插入图片描述
-gvalue:泛左值
- lvalue:左值,可以取地址的表达式

  • rvalue:右值
    • prvalue:纯右值,字面值如1,2,4,但是字符串字面值是左值,因为字符串字面值“abc”的本质是const char (*)[4]
    • xvalue:将亡值
      主要是了解将亡值(xvalue)
      将亡值:
  • 返回右值引用的调用表达式
  • 转换为右值引用的转换函数调用表达式如std::move

移动语义

移动语义的主要应用场景就是处理将亡值。当编译器遇到一个将亡值时,它可以选择调用移动构造函数或者移动赋值运算符来高效地转移资源,而不是调用拷贝构造函数进行复制。因为将亡值即将被销毁,所以将其资源转移给其他对象是一种安全且高效的操作。移动语义可以在处理大型数据结构或资源密集型对象时避免深度拷贝,从而显著提升性能。
代码:

#include <iostream>
#include <vector>

class MoveAble
{
public:
    std::vector<int> _data;
    MoveAble(std::vector<int> && input) noexcept : _data(std::move(input)) {
        std::cout << "move input to data" <<std::endl;
    }
    MoveAble(MoveAble && other) noexcept {
        // 右值引用经过函数传递后,会丢失右值特性,所以需要使用std::move
        _data = std::move(other._data);
        std::cout << "move construct" <<std::endl;
    }
};

int main()
{
    std::vector<int> input = {1,2,3,4,5};
    MoveAble test(std::move(input));
    MoveAble test2(std::move(test));
}

移动语义与拷贝优化(Copy Elision)

在 C++17 之前,RVO 是一个可选优化,但在 C++17 标准之后,RVO 被强制启用,编译器必须在符合条件的情况下执行拷贝省略。std::move 的出现增加了对于对象资源管理的精细控制,但其滥用可能会破坏编译器的优化。
使用std::move在返回值时会阻止编译器进行RVO或NRVO。这是因为std::move强制将对象视为右值,即使它是一个局部变量。编译器必须假设这个对象的资源可能已经被外部引用,因此不能在原地构造返回值。虽然std::move在某些情况下可以提高性能,如在函数接受右值引用参数时,但在返回局部变量时使用它将阻止RVO或NRVO,导致不必要的移动或拷贝操作,从而降低程序性能。
最佳实践:
当返回局部对象时,应该避免使用std::move,以便编译器可以尽可能地应用RVO或NRVO。只需简单地返回对象即可
当需要进行传参的时候

#include <iostream>
#include <vector>
#include "./testclass.hpp"

Vector GetVector(int size)
{
    Vector vec(size);
    return vec;
}

Vector GetVector2(int size)
{
    Vector vec(size);
    return std::move(vec);
}

int main()
{
    // 测试移动构造函数
    Vector input(10);
    TestClass test(std::move(input));
    TestClass test2(std::move(test));

    std::cout << "----------------" << std::endl;
    // 测试返回值优化
    Vector vec = GetVector(10);
    std::cout << "----------------" << std::endl;
    Vector vec2 = GetVector2(10);
    // 可以看到,使用了move之后,返回值优化失效,相较于GetVector函数,GetVector2函数多了一次move构造函数的调用
}

noexpect关键字

noexcept关键字告诉编译器该函数不会抛出异常,方便编译器进行更多优化。
C++的异常处理是在运行时检测的,编译器需要添加额外代码,影响优化,C++11以后使用noexcept来表示该函数不会抛出异常

此外,C++ 标准库容器(如 std::vector)在扩容或重新分配内存时,会优先选择使用 noexcept 的移动构造函数。对于不带 noexcept 的移动构造函数,std::vector 会选择拷贝而不是移动,以确保异常安全性。

在以下情况推荐使用noexcept:

  • 移动构造函数(move constructor)
  • 移动分配函数(move assignment)
  • 析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept
  • 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。

最佳实践:
在移动构造和移动赋值中使用 noexcept:在定义移动构造函数和移动赋值运算符时,若确保无异常抛出,应加上 noexcept,以便标准库容器能够更高效地处理对象。

Perfect Forwarding(完美转发/精确传递)

TestClass(Vector && input) noexcept : _data(input) {
        std::cout << "move input to data" <<std::endl;
    }

这么写一个成员对象移动构造函数对吗?
答案是不对,右值引用经过参数传递后会丢失右值性,所以我们需要再次调用move来变为右值

TestClass(Vector && input) noexcept : _data(std::move(input)) {
        std::cout << "move input to data" <<std::endl;
    }

完美转发就是将输入进来的右值转发给自己内部调用该右值的地方

TestClass(Vector && input) noexcept : _data(std::forward<Vector>(input)) {
        std::cout << "move input to data" <<std::endl;
    }

与std::move()相区别的是,move()会无条件的将一个参数转换成右值,而forward()则会保留参数的左右值类型。
所以,上面的这种情景并不是forward的常用情景,forward主要是配合模板使用。模板参数T&&可以出发引用折叠,配合std::forward可以实现完美转发效果,即不管进来的是什么类型的引用都可以以原来的类型转发过去。

#include <iostream>

template <typename T>
void Print(T& t)
{
    std::cout << "left reference" << std::endl;
}

template <typename T>
void Print(T&& t)
{
    std::cout << "right reference" << std::endl;
}

template <typename T>
void PerfectFroward(T&& t)
{
    Print(t); // 永远调用左值引用版
    Print(std::move(t)); // 永远调用右值引用版
    Print(std::forward<T>(t)); // 根据输入的类型,调用左值引用版或右值引用版
}

int main()
{
    int b=20,c=30;
    PerfectFroward(b); // 调用左值引用版
    std::cout << "----------------" << std::endl;
    PerfectFroward(std::move(c)); // 调用右值引用版
    return 0;
}

标准库glibc中forward的实现

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

当我们输入的是左值的时候,T自动推导为T&,则

    constexpr T& &&
    forward(typename std::remove_reference<T&>::type& __t) noexcept
    { return static_cast<T& &&>(__t); }
// 推导结果:
    constexpr T&
    forward(T & __t) noexcept
    { return static_cast<T&>(__t); }

带入到原始代码里

template <typename T>
void PerfectFroward(T&& t)
//折叠结果:void PerfectFroward(T& t)
{
    Print(std::forward<T>(t));
    //折叠结果:Print(T& t)
}

右值时同理,T自动推导为T&&,各处折叠如上折叠规则。

完美转发的本质目的是将左值引用右值引用统一签名,节约代码量

代码

#include <iostream>


/// @brief 对象五原则:拷贝(移动)构造函数、拷贝(移动)赋值函数、析构函数
///        如果要自己实现其中一个,就要自己实现其他, 除非有特殊需求
class Vector
{
    public:
        /// @brief 默认构造函数
        Vector():_size(0), _data(nullptr){}
        
        /// @brief 构造函数
        Vector(int size):_size(size){ 
            std::cout << "malloc" << std::endl;
            _data = new int[size];
        }
        
        /// @brief 拷贝构造函数
        Vector(const Vector& other):_size(other._size){
            std::cout << "malloc" << std::endl;
            _data = new int[_size];
            for(int i = 0; i < _size; i++){
                _data[i] = other._data[i];
            }
        }

        /// @brief 移动构造函数
        Vector(Vector&& other):_size(other._size), _data(other._data){
            std::cout << "move construct" << std::endl;
            other._size = 0;
            other._data = nullptr;
        }

        /// @brief 析构函数
        ~Vector(){
            if(_data != nullptr){
                delete[] _data;
            }
        }

        /// @brief 拷贝赋值函数 
        Vector& operator=(const Vector& other){
            if(this == &other){
                return *this;
            }
            if(_data != nullptr){
                delete[] _data;
            }
            _size = other._size;
            std::cout << "malloc" << std::endl;
            _data = new int[_size];
            for(int i = 0; i < _size; i++){
                _data[i] = other._data[i];
            }
            return *this;
        }

        /// @brief 移动赋值函数
        Vector& operator=(Vector&& other){
            if(this == &other){
                return *this;
            }
            if(_data != nullptr){
                delete[] _data;
            }
            _size = other._size;
            _data = other._data;
            other._size = 0;
            other._data = nullptr;
            return *this;
        }
    private:
        int* _data;
        int _size;
};


class TestClass
{
public:
    Vector _data;
    TestClass(Vector && input) noexcept : _data(std::forward<Vector>(input)) {
        std::cout << "move input to data" <<std::endl;
    }
    TestClass(TestClass && other) noexcept { //移动构造函数
        // 右值引用经过函数传递后,变成左值,丢失右值特性,所以需要使用std::move
        _data = std::move(other._data);
        std::cout << "move construct" <<std::endl;
    }
    TestClass(const TestClass& other){ //拷贝构造函数
        _data = other._data;
        std::cout << "copy construct" <<std::endl;
    }
};
举报

相关推荐

0 条评论