Move-semantics เทียบกับการอ้างอิง const [ซ้ำกัน]
คลาสของฉันมีตัวแปรสตริงและฉันต้องการเริ่มต้นด้วยค่าที่ส่งไปยังตัวสร้าง
ครูของฉันคิดว่าเราจะส่งสตริงเป็น const-reference:
MyClass::MyClass(const std::string &title){
this->title = title
}
อย่างไรก็ตาม Clang-Tidy แนะนำให้ใช้คำสั่ง move:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
ดังนั้นฉันจึงสงสัยว่าวิธีที่ถูกต้องในการทำ C ++ สมัยใหม่คืออะไร
ฉันมองไปรอบ ๆ แล้ว แต่ไม่มีอะไรตอบคำถามของฉันได้เลย ขอบคุณล่วงหน้า!
คำตอบ
ไม่มีใครที่ดีที่สุดตั้งแต่พวกเขาทั้งสองสร้างเริ่มต้นtitle
ครั้งแรกและจากนั้นคัดลอกกำหนดหรือย้ายกำหนดมัน ใช้รายการเริ่มต้นสมาชิก
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
วินาทีด้วยวิธีใดวิธีหนึ่งต่อไปนี้:
- สมาชิกถูกสร้างสำเนา
- A
std::string
ถูกสร้างโดยคอนสตรัคเตอร์การstd::string
แปลงจากนั้นสมาชิกจะถูกสร้างขึ้นโดยการคัดลอก
# 2 - คอนสตรัคเตอร์การแปลงตัวเดียวstd::string
โดยใช้ค่า
MyClass(std::string title) : title(std::move(title)) {}
สิ่งนี้จะสร้าง 1 หรือ 2 std::string
วินาทีด้วยวิธีใดวิธีหนึ่งต่อไปนี้:
- อาร์กิวเมนต์ถูกสร้างโดยการเพิ่มประสิทธิภาพค่าส่งคืนจากชั่วคราว (
str1
+str2
) จากนั้นสมาชิกจะถูกสร้างขึ้น - อาร์กิวเมนต์เป็นสำเนาที่สร้างขึ้นจากนั้นสมาชิกจะถูกสร้างขึ้น
- อาร์กิวเมนต์ถูกสร้างขึ้นจากนั้นสมาชิกจะถูกสร้างขึ้น
- อาร์กิวเมนต์ถูกสร้างโดยตัว
std::string
สร้างการแปลงจากนั้นสมาชิกจะถูกสร้างขึ้น
# 3 - การรวมตัวสร้างการแปลงสองตัว
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
สิ่งนี้จะสร้าง 1 หรือ 2 std::string
วินาทีด้วยวิธีใดวิธีหนึ่งต่อไปนี้:
- สมาชิกถูกสร้างสำเนา
- สมาชิกถูกสร้างขึ้น
- A
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)...) {}
สิ่งนี้จะสร้าง 1 std::string
like ใน # 4 เสมอ แต่ทั้งหมดจะทำผ่านตัวสร้างการส่งต่อ
- สมาชิกถูกสร้างสำเนา
- สมาชิกถูกสร้างขึ้น
- สมาชิกถูกสร้างโดยคอนสตรัคเตอร์
std::string
(อาจจะแปลง)
# 6 - ตัวสร้างการแปลงการส่งต่ออาร์กิวเมนต์เดียว
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
สิ่งนี้จะสร้าง 1 std::string
like ใน # 4 และ # 5 เสมอ แต่จะใช้อาร์กิวเมนต์เดียวและส่งต่อไปยังผู้std::string
สร้าง
- สมาชิกถูกสร้างสำเนา
- สมาชิกถูกสร้างขึ้น
- สมาชิกถูกสร้างโดยตัว
std::string
สร้างการแปลง
#6
สามารถใช้ตัวเลือกเพื่อทำการส่งต่อที่สมบูรณ์แบบได้อย่างง่ายดายหากคุณต้องการรับอาร์กิวเมนต์หลายตัวในตัวMyClass
สร้าง สมมติว่าคุณมี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))
{}
การคัดลอกการอ้างอิงจะสร้างสำเนาของตัวแปรดั้งเดิม (ตัวแปรดั้งเดิมและตัวแปรใหม่อยู่ในพื้นที่ที่ต่างกัน) การย้ายตัวแปรโลคัลไปยังค่าตัวแปรโลคัลของคุณ (และอีกครั้งเดิมและตัวแปรใหม่อยู่ในพื้นที่ที่ต่างกัน)
จากมุมมองของคอมไพเลอร์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 คือสตริงสมาชิกของคุณในชั้นเรียน
คุณสามารถค้นหาความช่วยเหลือที่เป็นประโยชน์ได้ที่นี่: รายชื่อผู้เริ่มต้นสมาชิกตัวสร้าง
มี 2 กรณี: lvalueหรือrvalueของstd::string
.
ในstd::string const&
รุ่นlvalueกรณีก็พอมีประสิทธิภาพผ่านอ้างอิงและคัดลอกแล้ว แต่ค่าrvalueจะถูกคัดลอกแทนที่จะย้ายซึ่งมีประสิทธิภาพต่ำกว่ามาก
ในstd::string
เวอร์ชันlvalueจะถูกคัดลอกเมื่อผ่านแล้วย้ายไปยังสมาชิก rvalueจะถูกย้ายสองครั้งในกรณีนี้ แต่โดยทั่วไปแล้วราคาถูกตัวสร้างการเคลื่อนย้าย
นอกจากนี้ในstd::string&&
เวอร์ชันไม่สามารถรับค่าlvalueได้ แต่rvalueจะถูกส่งผ่านโดยการอ้างอิงแล้วย้ายดีกว่าย้ายสองครั้ง
เห็นได้ชัดว่าเป็นแนวทางปฏิบัติที่ดีที่สุดสำหรับทั้งสองอย่างconst&
และ&&
เช่นเดียวกับที่ STL ทำอยู่เสมอ แต่ถ้าตัวสร้างการเคลื่อนย้ายมีราคาถูกเพียงพอเพียงแค่ส่งผ่านค่าและการเคลื่อนย้ายก็เป็นที่ยอมรับ