Pindah-semantik vs referensi const [duplikat]
kelas saya memiliki variabel string dan saya ingin menginisialisasi mereka dengan nilai yang diteruskan ke konstruktor.
Guru saya mengira kami meneruskan string sebagai referensi konstanta:
MyClass::MyClass(const std::string &title){
this->title = title
}
Namun Clang-Tidy menyarankan untuk menggunakan perintah pindah:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
Jadi saya bertanya-tanya apa cara yang benar untuk melakukan ini di C ++ modern.
Saya sudah melihat sekeliling, tetapi tidak ada yang benar-benar menjawab pertanyaan saya. Terima kasih sebelumnya!
Jawaban
Tidak ada yang optimal karena keduanya membangun default title
terlebih dahulu lalu menyalin tugaskan atau pindahkan menetapkannya . Gunakan daftar penginisialisasi anggota.
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
Mari kita lihat dan lihat apa yang terjadi di C ++ 17:
# 1 - Satu konstruktor pengubah yang mengambil file const&
.
MyClass::MyClass(const std::string& title) : title(title) {}
Ini akan membuat 1 atau 2 std::string
detik dengan salah satu cara berikut:
- Anggota adalah salinan yang dibuat.
- A
std::string
dikonstruksi oleh konstruktorstd::string
pengubah dan anggota dikonstruksikan.
# 2 - Konstruktor konversi tunggal yang mengambil std::string
nilai menurut.
MyClass(std::string title) : title(std::move(title)) {}
Ini akan membuat 1 atau 2 std::string
detik dengan salah satu cara berikut:
- Argumennya dibangun dengan optimasi nilai kembalian dari sementara (
str1
+str2
) dan kemudian anggota dipindahkan. - Argumennya adalah salinan dikonstruksi dan kemudian anggota tersebut dipindahkan.
- Argumennya adalah move dikonstruksi dan kemudian member tersebut bergerak dikonstruksi.
- Argumen dikonstruksikan oleh konstruktor
std::string
konversi dan kemudian anggota dipindahkan.
# 3 - Menggabungkan dua konstruktor pengubah.
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
Ini akan membuat 1 atau 2 std::string
detik dengan salah satu cara berikut:
- Anggota adalah salinan yang dibuat.
- Anggota bergerak dibangun.
- A
std::string
dikonstruksi oleh konstruktorstd::string
pengubah dan anggota dipindahkan.
Sejauh ini, opsi #3
tampaknya menjadi opsi paling efisien. Mari kita periksa beberapa opsi lagi.
# 4 - Sukai # 3 tetapi mengganti konstruktor konversi bergerak dengan konstruktor penerusan.
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
Ini akan selalu membuat 1 std::string
dengan salah satu cara berikut:
- Anggota adalah salinan yang dibangun melalui
A
. - Anggota dipindahkan melalui
B
. - Anggota dibangun oleh
std::string
(mungkin mengubah) konstruktor melaluiB
.
# 5 - Hanya konstruktor penerusan - menghapus konstruktor konversi penyalinan dari # 4.
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
Ini akan selalu membuat 1 std::string
seperti di # 4, tetapi semua dilakukan melalui konstruktor penerusan.
- Anggota adalah salinan yang dibuat.
- Anggota bergerak dibangun.
- Anggota dibangun oleh konstruktor
std::string
(mungkin mengubah).
# 6 - Konstruktor konversi penerusan argumen tunggal.
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
Ini akan selalu membuat 1 std::string
seperti di # 4 dan # 5 tetapi hanya akan mengambil satu argumen dan meneruskannya ke std::string
konstruktor.
- Anggota adalah salinan yang dibuat.
- Anggota bergerak dibangun.
- Anggota dibangun oleh
std::string
konstruktor pengubah.
Opsi #6
dapat dengan mudah digunakan untuk melakukan penerusan sempurna jika Anda ingin mengambil banyak argumen dalam MyClass
konstruktor. Misalkan Anda memiliki int
anggota dan std::string
anggota lain :
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}
Menyalin referensi membuat salinan dari variabel asli (asli dan yang baru berada di area yang berbeda), memindahkan variabel lokal ke nilai r variabel lokal Anda (dan sekali lagi, asli dan yang baru berada di area yang berbeda).
Dari sudut pandang penyusun, move
mungkin (dan sekarang) lebih cepat:
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
diterjemahkan menjadi:
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
Namun,
void MyClass(std::string& title){
std::string title = title;
}
menghasilkan kode yang lebih besar (serupa untuk 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"
Jadi iya, std::move
lebih baik (dalam situasi seperti ini).
Tidak apa-apa gunakan referensi const, lalu gunakan daftar penginisialisasi anggota:
MyClass(const std::string &title) : m_title{title}
Di mana m_title adalah string anggota Anda di kelas.
Anda dapat menemukan bantuan yang berguna di sini: Daftar penginisialisasi anggota pembuat
Ada 2 kasus: lvalue atau rvalue dari std::string
.
dalam std::string const&
versi, lvalue case cukup efisien, diteruskan dengan referensi dan kemudian disalin . tetapi nilai r akan disalin daripada dipindahkan , yang efisiensinya jauh lebih rendah.
di std::string
versi, lvalue yang disalin ketika melewati , dan kemudian pindah ke anggota. rvalue akan dipindahkan dua kali dalam kasus ini. tapi umumnya harganya murah , konstruktor bergerak.
selain itu, di std::string&&
versi, tidak dapat menerima lvalue , tapi nilai p adalah dikirimkan dengan referensi dan kemudian pindah , lebih baik dari pindah dua kali.
jadi jelas, ini adalah praktik terbaik dengan const&
dan &&
, seperti yang selalu dilakukan STL. tetapi jika konstruktor bergerak cukup murah, hanya melewatkan nilai dan memindahkan juga dapat diterima.