Post

move使用指南

std::move使用指南

  1. 使用move在不实现它两个配套函数,编译器合成,逐个调用move,如果底层类型没有move,那其实还是copy。这种在有指针的时候要尤其注意,指针指向的内存到底在哪里,在这次move之后还有意义吗。

  2. 配套move构造函数和拷贝构造和拷贝assign的操作是一样的时候。move我认为是没有意义。

不如看看rust的转移是怎么做的。i32和f64之类的基础变量,默认实现了copy。在很多数据在堆上的的复杂数据结构(我的理解其实就是c++里的对象了),所有权是转移的。

在这个基础上,我去理解move。c++的move仅仅是一个强转,我的理解下,他的意义就是单纯的为了匹配到相应的重载。可以参考CS106L实现的hashmap

真正所有去权的转移,是在配套函数内。比如持有一个char的string的move函数,持有者应该将char的所有权转移,并将指针设置空。

其实说到底,还是对与堆上分配的内存的自我管理。

move失效的问题

先看一段代码

1
2
3
4
5
6
7
std::vector<int> vec = {1,2,3};

auto func = [=](){
    auto vec2 = std::move(vec);
    std::cout << vec.size() << std::endl; // 输出:3
    std::cout << vec2.size() << std::endl; // 输出:3
};

此时的看起来失效了,原因只可能是move没有强转成vector&&。

看一下std::move的实现:

1
2
3
4
5
6
template<typename T>
decltype(auto) move(T&& param)
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

此时确实成功转型了,但是确是const vector&&,原因就是lambda闭包原理。lambda会转成一个闭包,即一个实现了operator()的functor类。但是这个运算符重载一般不做额外声明是const的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 转换前
auto func = [=](){
    auto vec2 = std::move(vec);
};

// 转换后
class Functor{
public:
    void operator() const{
        auto vec2 = std::move(vec);
    };

private:
    std::vector<int> vec;
};

解决方案也比较简单,直接另lambda为mutable就可以了。使用mutable的时候,()是不能省的。

1
2
3
4
5
6
7
std::vector<int> vec = {1,2,3};

auto func = [=]() mutable{
    auto vec2 = std::move(vec);
    std::cout <<vec.size() << std::endl; // 输出:0
    std::cout <<vec2.size() << std::endl; // 输出:3
};

同时,cpp11应该尽量避免move capture。

std::function的拷贝和移动

std::function对象和普通的模版类对象一样,可以执行拷贝构造

1
std::function<void()> funcb = funca;

拷贝构造时是做了逐成员的拷贝构造。赋值同理

1
2
std::function<void()> funcc;
funcc = funca;

移动构造也是如此,逐对象进行移动构造。如果没有move函数,但又显式声明了const class&的拷贝构造函数,其实调用的是拷贝构造函数。

1
std::function<void()> funcd = std::move(funca);

return std::move(var_)

大多数情况下返回一个move是没啥意义的,因为编译器会优化掉,前提是这个变量是个prvalue,这次调用结束之后就gg了。

只有需要返回类的成员变量的右值引用的时候才返回一个右值,数据成员不是隐式可移动实体,如果不std::move,直接return x,重载决议不会选择移动构造。

比如:

1
2
3
4
5
6
class x {
    std::string s_;
    std::string release() {
        return std::move(s_);
    }
}

类中存在自指向的指针时,或者类中存在指针时慎用move

看这样一个类

1
2
3
4
5
6
7
8
9
10
struct foo {
  std::strig s;
  const char* data{nullptr};
};

foo fo;
f.s = "bilibili.com"
fo.data = s.data();

auto bar = std::move(fo);

此时调用编译器合成move构造函数,会将fo.data指向的内存释放掉,bar.data其实指向的是fo.s.data(),这样就出现了问题。

因此,在类中存在指针时,慎用move,尤其是存在自指向的指针时。

copy/move elision

其实经常有这个坑,编程时经常会写的一种函数叫做named constructor,这种函数的返回值是某个类的实例,其实本质上就是一种构造函数,但是因为可能需要在构建时执行一些其他的步骤,所以没有写成constructor的形式。

1
2
3
4
5
6
7
8
9
User create_user(const std::string &username, const std::string &password) { 
    User user(username, password);
    validate_and_save_to_db(user);
    return user;
} 
void signup(const std::string &username, const std::string &password) {
    auto new_user = create_user(username, password);
    login(user);
}

比较显然的是,create_user就是一个named ctor,如果单纯扣定义的话,这里会有两次拷贝构造,一次是create_user返回的时候,一次是signup函数中的new_user

所以第一个想法是,move优化。

但是,事实上,编译器比你更聪明,编译器可以直接把user创建在new_user里,所以user只被创建一次,没有任何copy开销,user和new_user经过编译器优化之后其实是同一个variable!这种优化就叫做copy elision。但是很不幸的是,如果用户想自己用move优化的话,编译器就不用做copy elision了,只能乖乖地按照用户说的来,先创建一个user,然后在调用User的move constructor来创建new_user。

不难知道这个move对比copy elision,消耗肯定变大了

copy elision是C++标准的一部分,当然如果你想关闭,也可以自己显式的指定-fno-elide-constructors来关闭这个优化, 这样之前的NRVO和URVO可能会走move构造函数。

什么情况下不会走copy elision

当然有这种情况,编译器也会存在无法优化的case,我们可以主要关注两种编译器100%会优化掉的case

  1. URVO(Unnamed Return Value Optimizatio), 函数的所有执行路径都返回同一个类型的匿名变量,比如
1
2
3
4
5
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 User{username, password};
}
  1. NRVO(Named Return Value Optimization), 函数的所有执行路径都返回同一个类型的命名变量,比如
1
2
3
4
5
6
7
8
9
10
11
12
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;
    }
}

其余情况可以按需要分析汇编,看看是否需要自己做move优化

Ref

  1. 返回move有意义吗?

  2. C++ 函数返回局部变量的std::move()问题?

This post is licensed under CC BY 4.0 by the author.

Trending Tags