Move-semantics so với tham chiếu const [trùng lặp]
lớp của tôi có các biến chuỗi và tôi muốn khởi tạo chúng bằng các giá trị được truyền cho hàm tạo.
Giáo viên của tôi nghĩ rằng chúng tôi truyền các chuỗi dưới dạng tham chiếu const:
MyClass::MyClass(const std::string &title){
this->title = title
}
Tuy nhiên Clang-Tidy đề nghị sử dụng lệnh di chuyển:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
Vì vậy, tôi tự hỏi cách chính xác để thực hiện việc này trong C ++ hiện đại là gì.
Tôi đã nhìn xung quanh, nhưng không có gì thực sự trả lời câu hỏi của tôi. Cảm ơn trước!
Trả lời
Không có gì là tối ưu vì cả hai đều xây dựng mặc định titletrước và sau đó sao chép gán hoặc di chuyển gán nó. Sử dụng danh sách khởi tạo thành viên.
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
Hãy xem xét chúng và xem điều gì sẽ xảy ra trong C ++ 17:
# 1 - Một hàm tạo chuyển đổi duy nhất lấy a const&.
MyClass::MyClass(const std::string& title) : title(title) {}
Điều này sẽ tạo ra 1 hoặc 2 std::stringgiây theo một trong những cách sau:
- Thành viên là bản sao được xây dựng.
- A
std::stringđược xây dựng bởi một phương thứcstd::stringkhởi tạo chuyển đổi và sau đó thành viên được tạo bản sao.
# 2 - Một hàm tạo chuyển đổi duy nhất nhận một std::stringgiá trị theo giá trị.
MyClass(std::string title) : title(std::move(title)) {}
Điều này sẽ tạo ra 1 hoặc 2 std::stringgiây theo một trong những cách sau:
- Đối số được xây dựng bằng cách tối ưu hóa giá trị trả về từ dấu (
str1+str2) tạm thời và sau đó thành viên được di chuyển được xây dựng. - Đối số được tạo bản sao và sau đó thành viên được di chuyển được xây dựng.
- Đối số được xây dựng di chuyển và sau đó thành viên được di chuyển được xây dựng.
- Đối số được xây dựng bởi một phương thức
std::stringkhởi tạo chuyển đổi và sau đó thành viên được xây dựng di chuyển.
# 3 - Kết hợp hai hàm tạo chuyển đổi.
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
Điều này sẽ tạo ra 1 hoặc 2 std::stringgiây theo một trong những cách sau:
- Thành viên là bản sao được xây dựng.
- Thành viên được xây dựng.
- A
std::stringđược xây dựng bởi một phương thứcstd::stringkhởi tạo chuyển đổi và sau đó thành viên được cấu trúc di chuyển.
Cho đến nay, quyền chọn #3dường như là lựa chọn hiệu quả nhất. Hãy kiểm tra thêm một số tùy chọn.
# 4 - Giống như # 3 nhưng thay thế hàm tạo chuyển đổi di chuyển bằng một hàm tạo chuyển tiếp.
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
Điều này sẽ luôn tạo ra 1 std::stringtrong một trong những cách sau:
- Thành viên được sao chép xây dựng thông qua
A. - Thành viên được xây dựng qua
B. - Thành viên được xây dựng bởi một phương thức khởi tạo
std::string(có thể chuyển đổi) thông quaB.
# 5 - Chỉ một hàm tạo chuyển tiếp - loại bỏ hàm tạo chuyển đổi sao chép khỏi # 4.
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
Điều này sẽ luôn tạo 1 std::stringgiống như trong # 4, nhưng tất cả được thực hiện thông qua phương thức khởi tạo chuyển tiếp.
- Thành viên là bản sao được xây dựng.
- Thành viên được xây dựng.
- Thành viên được xây dựng bởi một phương thức khởi tạo
std::string(có thể chuyển đổi).
# 6 - Một phương thức khởi tạo chuyển đổi chuyển tiếp đối số duy nhất.
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
Điều này sẽ luôn tạo ra 1 std::stringnhư trong # 4 và # 5 nhưng sẽ chỉ lấy một đối số và chuyển tiếp nó đến hàm std::stringtạo.
- Thành viên là bản sao được xây dựng.
- Thành viên được xây dựng.
- Thành viên được xây dựng bởi một phương thức
std::stringkhởi tạo chuyển đổi.
Tùy chọn #6có thể dễ dàng được sử dụng để thực hiện chuyển tiếp hoàn hảo nếu bạn muốn nhận nhiều đối số trong hàm MyClasstạo. Giả sử bạn có một intthành viên và một std::stringthành viên khác :
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}
Việc sao chép tham chiếu sẽ tạo ra một bản sao của biến ban đầu (biến ban đầu và biến mới nằm trên các khu vực khác nhau), di chuyển giá trị của biến cục bộ sang giá trị của biến cục bộ của bạn (và một lần nữa, biến gốc và biến mới nằm trên các khu vực khác nhau).
Từ quan điểm của trình biên dịch, movecó thể (và đang) nhanh hơn:
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
Dịch sang:
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
Tuy nhiên,
void MyClass(std::string& title){
std::string title = title;
}
tạo mã lớn hơn (tương tự cho 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"
Vì vậy, có, std::movelà tốt hơn (trong những trường hợp này).
Nó được sử dụng một tham chiếu const, sau đó sử dụng danh sách khởi tạo thành viên:
MyClass(const std::string &title) : m_title{title}
Trong đó m_title là chuỗi thành viên của bạn trong lớp.
Bạn có thể tìm sự giúp đỡ hữu ích ở đây: danh sách khởi tạo thành viên Constructor
có 2 trường hợp: lvalue hoặc rvalue của std::string.
trong std::string const&phiên bản, trường hợp giá trị là đủ hiệu quả, được chuyển bằng tham chiếu và sau đó được sao chép . nhưng một rvalue sẽ được sao chép chứ không phải di chuyển , đó là hiệu quả thấp hơn nhiều.
trong std::stringphiên bản, lvalue được sao chép khi được thông qua , và sau đó được chuyển đến thành viên. rvalue sẽ được di chuyển hai lần trong trường hợp này. nhưng nói chung là nó rẻ , công cụ khởi tạo di chuyển.
bên cạnh đó, trong std::string&&phiên bản, nó có thể không nhận được một giá trị trái , nhưng rvalue được thông qua tham khảo và sau đó di chuyển , tốt hơn so với di chuyển hai lần.
nên rõ ràng, đó là cách tốt nhất với cả hai const&và &&, như những gì STL luôn làm. nhưng nếu phương thức khởi tạo move đủ rẻ, chỉ cần truyền giá trị và di chuyển cũng được chấp nhận.