移动语义与常量引用
Move-semantics vs const reference
我的 class 有字符串变量,我想用传递给构造函数的值初始化它们。
我的老师认为我们将字符串作为常量引用传递:
MyClass::MyClass(const std::string &title){
this->title = title
}
但是 Clang-Tidy 建议使用移动命令:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
所以我想知道在现代 C++ 中执行此操作的正确方法是什么。
我已经环顾四周,但没有任何东西能真正回答我的问题。提前致谢!
复制引用会创建原始变量的副本(原始变量和新变量位于不同的区域),移动局部变量会将局部变量强制转换为右值(同样,原始变量和新变量位于不同的区域)地区)。
从编译器的角度来看,move
可能(现在)更快:
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
转换为:
MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
sub rsp, 40
mov rax, rdi
lea rcx, [rsp + 24]
mov qword ptr [rsp + 8], rcx
mov rdi, qword ptr [rdi]
lea rdx, [rax + 16]
cmp rdi, rdx
je .LBB0_1
mov qword ptr [rsp + 8], rdi
mov rsi, qword ptr [rax + 16]
mov qword ptr [rsp + 24], rsi
jmp .LBB0_3
.LBB0_1:
movups xmm0, xmmword ptr [rdi]
movups xmmword ptr [rcx], xmm0
mov rdi, rcx
.LBB0_3:
mov rsi, qword ptr [rax + 8]
mov qword ptr [rsp + 16], rsi
mov qword ptr [rax], rdx
mov qword ptr [rax + 8], 0
mov byte ptr [rax + 16], 0
cmp rdi, rcx
je .LBB0_5
call operator delete(void*)
.LBB0_5:
add rsp, 40
ret
然而,
void MyClass(std::string& title){
std::string title = title;
}
生成更大的代码(类似于 GCC):
MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
push r15
push r14
push rbx
sub rsp, 48
lea r15, [rsp + 32]
mov qword ptr [rsp + 16], r15
mov r14, qword ptr [rdi]
mov rbx, qword ptr [rdi + 8]
test r14, r14
jne .LBB0_2
test rbx, rbx
jne .LBB0_11
.LBB0_2:
mov qword ptr [rsp + 8], rbx
mov rax, r15
cmp rbx, 16
jb .LBB0_4
lea rdi, [rsp + 16]
lea rsi, [rsp + 8]
xor edx, edx
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)
mov qword ptr [rsp + 16], rax
mov rcx, qword ptr [rsp + 8]
mov qword ptr [rsp + 32], rcx
.LBB0_4:
test rbx, rbx
je .LBB0_8
cmp rbx, 1
jne .LBB0_7
mov cl, byte ptr [r14]
mov byte ptr [rax], cl
jmp .LBB0_8
.LBB0_7:
mov rdi, rax
mov rsi, r14
mov rdx, rbx
call memcpy
.LBB0_8:
mov rax, qword ptr [rsp + 8]
mov qword ptr [rsp + 24], rax
mov rcx, qword ptr [rsp + 16]
mov byte ptr [rcx + rax], 0
mov rdi, qword ptr [rsp + 16]
cmp rdi, r15
je .LBB0_10
call operator delete(void*)
.LBB0_10:
add rsp, 48
pop rbx
pop r14
pop r15
ret
.LBB0_11:
mov edi, offset .L.str
call std::__throw_logic_error(char const*)
.L.str:
.asciz "basic_string::_M_construct null not valid"
所以是的,std::move
更好(在这种情况下)。
可以使用 const 引用,然后使用成员初始化列表:
MyClass(const std::string &title) : m_title{title}
其中 m_title 是您在 class 中的成员字符串。
您可以在这里找到有用的帮助:Constructor member initializer lists
有2种情况:左值或右值 of std::string
.
在std::string const&
版本中,左值大小写足够高效,通过引用传递然后复制。但是 rvalue 将被 copied 而不是 moved,效率要低得多。
在std::string
版本中,左值在传递时被复制,然后移动 给会员。在这种情况下,右值 将移动两次。但通常它是便宜,移动构造函数。
此外,在std::string&&
版本中,它不能接收lvalue,但是rvalue是通过引用传递然后移动,比移动两次更好
很明显,这是 const&
和 &&
的最佳实践,就像 STL 一直做的那样。但是如果移动构造函数足够便宜,只传递值和移动也是可以接受的。
None 是最优的,因为它们都默认先构建 title
然后 copy assign 或 move assign它。使用成员初始值设定项列表。
MyClass::MyClass(const std::string& title) : title(title) {} // #1
// or
MyClass::MyClass(std::string title) : title(std::move(title)) {} // #2
//or
MyClass::MyClass(const std::string& title) : title(title) {} // #3
MyClass::MyClass(std::string&& title) : title(std::move(title)) {} // #3
让我们看看它们,看看 C++17 中会发生什么:
#1 - 采用 const&
.
的单个转换构造函数
MyClass::MyClass(const std::string& title) : title(title) {}
这将通过以下方式之一创建 1 或 2 个 std::string
:
- 该成员是复制构造的。
- 一个
std::string
由std::string
转换构造函数构造,然后复制构造该成员。
#2 - 一个转换构造函数,按值取 std::string
。
MyClass(std::string title) : title(std::move(title)) {}
这将通过以下方式之一创建 1 或 2 个 std::string
:
- 参数由return value optimization从临时(
str1
+ str2
)构造,然后移动构造成员。
- 参数是复制构造的,然后成员是移动构造的。
- 参数被移动构造,然后成员被移动构造。
- 参数由
std::string
转换构造函数构造,然后成员被移动构造。
#3 - 组合两个转换构造函数。
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
这将通过以下方式之一创建 1 或 2 个 std::string
:
- 该成员是复制构造的。
- 该成员是移动构造的。
- 一个
std::string
由一个std::string
转换构造函数构造,然后移动构造该成员。
到目前为止,选项 #3
似乎是最有效的选项。让我们再检查几个选项。
#4 - 与#3 类似,但将移动转换构造函数替换为转发构造函数。
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
这将始终以下列方式之一创建 1 std::string
:
- 该成员是通过
A
复制构造的。
- 该成员是通过
B
移动构建的。
- 该成员由
std::string
(可能转换)构造函数通过 B
构造。
#5 - 仅转发构造函数 - 从#4 中删除复制转换构造函数。
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
这将始终像#4 中那样创建 1 std::string
,但所有操作都是通过转发构造函数完成的。
- 该成员是复制构造的。
- 该成员是移动构造的。
- 该成员由
std::string
(可能转换)构造函数构造。
#6 - 单个参数转发转换构造函数。
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
这将始终创建 1 个 std::string
,就像在 #4 和 #5 中一样,但只会接受一个参数并将其转发给 std::string
构造函数。
- 该成员是复制构造的。
- 该成员是移动构造的。
- 该成员由
std::string
转换构造函数构造。
如果您想在 MyClass
构造函数中采用多个参数,则可以使用 选项 #6
轻松地进行完美转发。假设您有一个 int
成员和另一个 std::string
成员:
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}
我的 class 有字符串变量,我想用传递给构造函数的值初始化它们。
我的老师认为我们将字符串作为常量引用传递:
MyClass::MyClass(const std::string &title){
this->title = title
}
但是 Clang-Tidy 建议使用移动命令:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
所以我想知道在现代 C++ 中执行此操作的正确方法是什么。
我已经环顾四周,但没有任何东西能真正回答我的问题。提前致谢!
复制引用会创建原始变量的副本(原始变量和新变量位于不同的区域),移动局部变量会将局部变量强制转换为右值(同样,原始变量和新变量位于不同的区域)地区)。
从编译器的角度来看,move
可能(现在)更快:
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
转换为:
MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
sub rsp, 40
mov rax, rdi
lea rcx, [rsp + 24]
mov qword ptr [rsp + 8], rcx
mov rdi, qword ptr [rdi]
lea rdx, [rax + 16]
cmp rdi, rdx
je .LBB0_1
mov qword ptr [rsp + 8], rdi
mov rsi, qword ptr [rax + 16]
mov qword ptr [rsp + 24], rsi
jmp .LBB0_3
.LBB0_1:
movups xmm0, xmmword ptr [rdi]
movups xmmword ptr [rcx], xmm0
mov rdi, rcx
.LBB0_3:
mov rsi, qword ptr [rax + 8]
mov qword ptr [rsp + 16], rsi
mov qword ptr [rax], rdx
mov qword ptr [rax + 8], 0
mov byte ptr [rax + 16], 0
cmp rdi, rcx
je .LBB0_5
call operator delete(void*)
.LBB0_5:
add rsp, 40
ret
然而,
void MyClass(std::string& title){
std::string title = title;
}
生成更大的代码(类似于 GCC):
MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&): # @MyClass(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
push r15
push r14
push rbx
sub rsp, 48
lea r15, [rsp + 32]
mov qword ptr [rsp + 16], r15
mov r14, qword ptr [rdi]
mov rbx, qword ptr [rdi + 8]
test r14, r14
jne .LBB0_2
test rbx, rbx
jne .LBB0_11
.LBB0_2:
mov qword ptr [rsp + 8], rbx
mov rax, r15
cmp rbx, 16
jb .LBB0_4
lea rdi, [rsp + 16]
lea rsi, [rsp + 8]
xor edx, edx
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)
mov qword ptr [rsp + 16], rax
mov rcx, qword ptr [rsp + 8]
mov qword ptr [rsp + 32], rcx
.LBB0_4:
test rbx, rbx
je .LBB0_8
cmp rbx, 1
jne .LBB0_7
mov cl, byte ptr [r14]
mov byte ptr [rax], cl
jmp .LBB0_8
.LBB0_7:
mov rdi, rax
mov rsi, r14
mov rdx, rbx
call memcpy
.LBB0_8:
mov rax, qword ptr [rsp + 8]
mov qword ptr [rsp + 24], rax
mov rcx, qword ptr [rsp + 16]
mov byte ptr [rcx + rax], 0
mov rdi, qword ptr [rsp + 16]
cmp rdi, r15
je .LBB0_10
call operator delete(void*)
.LBB0_10:
add rsp, 48
pop rbx
pop r14
pop r15
ret
.LBB0_11:
mov edi, offset .L.str
call std::__throw_logic_error(char const*)
.L.str:
.asciz "basic_string::_M_construct null not valid"
所以是的,std::move
更好(在这种情况下)。
可以使用 const 引用,然后使用成员初始化列表:
MyClass(const std::string &title) : m_title{title}
其中 m_title 是您在 class 中的成员字符串。
您可以在这里找到有用的帮助:Constructor member initializer lists
有2种情况:左值或右值 of std::string
.
在std::string const&
版本中,左值大小写足够高效,通过引用传递然后复制。但是 rvalue 将被 copied 而不是 moved,效率要低得多。
在std::string
版本中,左值在传递时被复制,然后移动 给会员。在这种情况下,右值 将移动两次。但通常它是便宜,移动构造函数。
此外,在std::string&&
版本中,它不能接收lvalue,但是rvalue是通过引用传递然后移动,比移动两次更好
很明显,这是 const&
和 &&
的最佳实践,就像 STL 一直做的那样。但是如果移动构造函数足够便宜,只传递值和移动也是可以接受的。
None 是最优的,因为它们都默认先构建 title
然后 copy assign 或 move assign它。使用成员初始值设定项列表。
MyClass::MyClass(const std::string& title) : title(title) {} // #1
// or
MyClass::MyClass(std::string title) : title(std::move(title)) {} // #2
//or
MyClass::MyClass(const std::string& title) : title(title) {} // #3
MyClass::MyClass(std::string&& title) : title(std::move(title)) {} // #3
让我们看看它们,看看 C++17 中会发生什么:
#1 - 采用 const&
.
MyClass::MyClass(const std::string& title) : title(title) {}
这将通过以下方式之一创建 1 或 2 个 std::string
:
- 该成员是复制构造的。
- 一个
std::string
由std::string
转换构造函数构造,然后复制构造该成员。
#2 - 一个转换构造函数,按值取 std::string
。
MyClass(std::string title) : title(std::move(title)) {}
这将通过以下方式之一创建 1 或 2 个 std::string
:
- 参数由return value optimization从临时(
str1
+str2
)构造,然后移动构造成员。 - 参数是复制构造的,然后成员是移动构造的。
- 参数被移动构造,然后成员被移动构造。
- 参数由
std::string
转换构造函数构造,然后成员被移动构造。
#3 - 组合两个转换构造函数。
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
这将通过以下方式之一创建 1 或 2 个 std::string
:
- 该成员是复制构造的。
- 该成员是移动构造的。
- 一个
std::string
由一个std::string
转换构造函数构造,然后移动构造该成员。
到目前为止,选项 #3
似乎是最有效的选项。让我们再检查几个选项。
#4 - 与#3 类似,但将移动转换构造函数替换为转发构造函数。
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
这将始终以下列方式之一创建 1 std::string
:
- 该成员是通过
A
复制构造的。 - 该成员是通过
B
移动构建的。 - 该成员由
std::string
(可能转换)构造函数通过B
构造。
#5 - 仅转发构造函数 - 从#4 中删除复制转换构造函数。
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
这将始终像#4 中那样创建 1 std::string
,但所有操作都是通过转发构造函数完成的。
- 该成员是复制构造的。
- 该成员是移动构造的。
- 该成员由
std::string
(可能转换)构造函数构造。
#6 - 单个参数转发转换构造函数。
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
这将始终创建 1 个 std::string
,就像在 #4 和 #5 中一样,但只会接受一个参数并将其转发给 std::string
构造函数。
- 该成员是复制构造的。
- 该成员是移动构造的。
- 该成员由
std::string
转换构造函数构造。
如果您想在
MyClass
构造函数中采用多个参数,则可以使用 选项 #6
轻松地进行完美转发。假设您有一个 int
成员和另一个 std::string
成员:
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}