近期代码里一些疑惑点
- std::move/std::forward 并不会将原对象置为空
1 2 3 4 5 6 7 8 9
| void function(std::string&& remark) { ... }
void function_source(std::string&& source) { for (auto & e : v) { function(std::forward<std::string>(source)); } }
|
如上,循环里使用 std::forward,在function
不会做 std::move
动作的情况下,这个写法是可行的
当然还是不建议这么写,因为 remark
是可以被修改的…调用方不应留这样的隐患,还是用 const std::string&
更好
- std::move 对于 std::function/lambda 的不同表现
1 2 3 4 5 6 7 8 9 10 11
| void function(std::function<void()>&& oper) {
}
void function_source() { auto source = []() { ... }; function(std::move(source)); function(std::move(source)); }
|
如上,对source
进行两次 std::move
,会得到两个不同的std::function
对象,而不会得到一个空对象
对上面的问题做了一些测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| struct helper_st { int i = 0; helper_st() = default; helper_st(helper_st &&) { std::cout << "struct move" << std::endl; i = 100; } helper_st(const helper_st &) { std::cout << "struct copy" << std::endl; i += 1; } };
void string_move_test(std::string &&str) { str.append("X"); std::cout << str << std::endl; }
void function_move_test(std::function<void()> &&func) { func(); std::cout << typeid(func).name() << " x " << std::addressof(func) << std::endl; }
void function_move_test(const std::function<void()> &func) { func(); std::cout << typeid(func).name() << "y" << std::endl; } void function_move_test_2(std::function<void()> &&func) { function_move_test(std::forward<std::function<void()>>(func)); function_move_test(std::forward<std::function<void()>>(func)); std::function<void()> move_func(std::move(func)); std::cout << (func ? "not nullptr\n" : "nullptr\n");
} int main(int argc, char *argv[]) { { std::string source("string"); std::cout << std::boolalpha; std::cout << source.empty() << std::endl; std::string target(std::move(source)); std::cout << source.empty() << std::endl; std::cout << target.empty() << std::endl; } { std::string source = "abc"; string_move_test(std::forward<std::string>(source)); string_move_test(std::forward<std::string>(source)); string_move_test(std::move(source)); string_move_test(std::forward<std::string>(source)); std::cout << source << std::endl; } {
auto ff = []() {}; std::function<void()> ff_v = []() {};
std::function<void()> fff{std::move(ff)}; std::function<void()> fff_v{std::move(ff_v)};
std::cout << (ff ? "not nullptr\n" : "nullptr\n"); std::cout << (ff_v ? "not nullptr\n" : "nullptr\n");
std::cout << typeid(ff).name() << "\n"; std::cout << typeid(fff_v).name() << "\n"; } { helper_st hst; auto func = [&hst]() { hst.i += 1; static int xx = 1; xx += 1; std::cout << "abc:" << hst.i << "\t" << xx << "\t"; }; function_move_test_2(std::forward<std::function<void()>>(func)); function_move_test(std::forward<std::function<void()>>(func)); function_move_test(std::move(func)); function_move_test(std::forward<std::function<void()>>(func)); func(); std::cout << std::endl; } { struct st_basic { virtual void v_function(int default_value = 0) { std::cout << "st_basic: " << default_value << '\n'; } }; struct st_child : public st_basic { virtual void v_function(int default_value = 1) override { std::cout << "st_child: " << default_value << '\n'; } };
std::shared_ptr<st_basic> sptr = std::make_shared<st_child>(); sptr->v_function(); } return 0; }
|
总结
std::move
相当于强制将对象转换为 rvalue
,这个右值是可以被修改的,传给其他函数时,只有这个函数对右值做了修改,才会对原对象产生影响,之前受 std::string
等有默认右值构造函数的影响,以为 std::move
会置空原对象是不对的….例如:string_move_test
函数参数是右值类型,实现是修改参数而非置空,就会得到如上输出结构
std::forward
同理
std::function
/lambda
是两个东西,lambda
-> std::function
会进行隐式转换,所以 std::move
会得到不同的对象,而不会置空原对象
在测试 helper_st hst;
引用到 lambda
表达式的情况看到是相同对象,也就是 lambda
-> std::function
只构造了新的函数指针,这里 hst
相当于 std::ref(hst)
;这也是符合理解的
然后想到 virtual function
的情况,用了一种不常见的方式,给虚函数加带默认值的参数….可以看到结果是使用了基类的函数地址 + 对象类型(父类)的参数默认值;
参数在编译期就确定了,而虚函数地址是运行期确定的,所以会得到这样的结果;
避免歧义,虚函数不要带默认值参数
引申:如果换成CRTP呢?那么都是编译期行为了,这个函数原本是参数是什么默认值就会有什么输出,所以不会出现上面的歧义
move & rvo
- 测试是以前做的了,前面有同事看到我做优化时候把他们写的
return std::move(...);
改成了 return ...;
; 就翻了下之前测试的东西补发出来
- 编译器优化,在编译期将右值转换为左值,以减少不必要的拷贝
- 之前也是习惯写成
return std::move(string_value);
但是被编译期警告了,应该改成 return string_value;
- 具体参考:https://en.cppreference.com/w/cpp/language/copy_elision
- 或者搜索
RVO
、 NRVO
、 copy elision
- 测试结果和说明都标记在图片里了: