C++ std::move & lambda & std::function & virtual

近期代码里一些疑惑点

  • 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"); // nullptr

}
int main(int argc, char *argv[]) {
// about string
// https://stackoverflow.com/questions/65426585/stdmovestdstring-not-making-passed-argument-to-empty-state
{
std::string source("string");
std::cout << std::boolalpha;
std::cout << source.empty() << std::endl; // false
std::string target(std::move(source));
std::cout << source.empty() << std::endl; // true
std::cout << target.empty() << std::endl; // false
}
{
std::string source = "abc";
string_move_test(std::forward<std::string>(source)); // abcX
string_move_test(std::forward<std::string>(source)); // abcXX
string_move_test(std::move(source)); // abcXXX
string_move_test(std::forward<std::string>(source)); // abcXXXX
std::cout << source << std::endl; // abcXXXX
}
// about std::function & lambda
// https://stackoverflow.com/questions/13680587/move-semantic-with-stdfunction
// https://sf-zhou.github.io/programming/lambda_implicit_conversion_bug.html
{

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"); // not nullptr
std::cout << (ff_v ? "not nullptr\n" : "nullptr\n"); // nullptr

std::cout << typeid(ff).name() << "\n"; // class `int __cdecl main(int,char * __ptr64 * __ptr64 const)'::`5'::<lambda_1>
std::cout << typeid(fff_v).name() << "\n"; // class std::function<void __cdecl(void)>
}
{
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)); // abc:1 2 class std::function<void __cdecl(void)> x 000000BC55DFCCA0
// abc:2 3 class std::function<void __cdecl(void)> x 000000BC55DFCCA0
function_move_test(std::forward<std::function<void()>>(func)); // abc:3 4 class std::function<void __cdecl(void)> x 000000BC55DFCD00
function_move_test(std::move(func)); // abc:4 5 class std::function<void __cdecl(void)> x 000000BC55DFCD60
function_move_test(std::forward<std::function<void()>>(func)); // abc:5 6 class std::function<void __cdecl(void)> x 000000BC55DFCDC0
func(); // abc:6 7
std::cout << std::endl;
}
// virtual function with default argument
// https://www.sandordargo.com/blog/2021/01/27/virtual-functions-with-default-arguments
{
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(); // st_child: 0
}
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
  • 或者搜索 RVONRVOcopy elision
  • 测试结果和说明都标记在图片里了:


------ 本文结束 ------
------ 版权声明:转载请注明出处 ------