Taşıma semantiği ve sabit referansı [yineleme]
Sınıfımın dize değişkenleri var ve bunları kurucuya aktarılan değerlerle başlatmak istiyorum.
Öğretmenim dizeleri bir const referansı olarak aktarmamızı düşündü:
MyClass::MyClass(const std::string &title){
this->title = title
}
Ancak Clang-Tidy, hareket komutunun kullanılmasını önerir:
MyClass::MyClass(std::string title){
this->title = std::move(title)
}
Bu yüzden modern C ++ 'da bunu yapmanın doğru yolunun ne olduğunu merak ediyorum.
Zaten etrafıma baktım ama hiçbir şey sorumu gerçekten cevaplamadı. Şimdiden teşekkürler!
Yanıtlar
Her ikisi de title
önce varsayılan oluşturduğundan ve sonra atamayı kopyaladığından veya atamayı taşıdığından hiçbiri optimal değildir. Üye başlatıcı listesini kullanı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
Onlara bakalım ve C ++ 17'de ne olduğunu görelim:
# 1 - Bir const&
.
MyClass::MyClass(const std::string& title) : title(title) {}
Bu, aşağıdaki std::string
yollardan biriyle 1 veya 2 s oluşturacaktır :
- Üye, kopya oluşturulmuştur.
- A
std::string
,std::string
dönüştüren bir kurucu tarafından oluşturulur ve daha sonra üye, kopya oluşturulur.
# 2 - Değere göre alan tek bir dönüştürücü kurucu std::string
.
MyClass(std::string title) : title(std::move(title)) {}
Bu, aşağıdaki std::string
yollardan biriyle 1 veya 2 s oluşturacaktır :
- Argüman, geçici bir ( + ) dönüş değeri optimizasyonu ile oluşturulur ve daha sonra üye hareket ettirilir.
str1
str2
- Argüman kopya yapılandırılır ve daha sonra üye taşınır.
- Argüman, hareket inşa edilir ve daha sonra üye hareket ettirilir.
- Argüman
std::string
dönüştüren bir kurucu tarafından oluşturulur ve ardından üye hareket ettirilir.
# 3 - İki dönüştürücü kurucuyu birleştirmek.
MyClass(const std::string& title) : title(title) {}
MyClass(std::string&& title) : title(std::move(title)) {}
Bu, aşağıdaki std::string
yollardan biriyle 1 veya 2 s oluşturacaktır :
- Üye, kopya oluşturulmuştur.
- Üye taşınır.
- A
std::string
,std::string
dönüştüren bir kurucu tarafından oluşturulur ve ardından üye taşınır.
Şimdiye kadar, seçenek #3
en verimli seçenek gibi görünüyor. Birkaç seçeneği daha kontrol edelim.
# 4 - # 3 gibi, ancak hareketli dönüşüm kurucusunu iletme kurucusuyla değiştirmek.
MyClass(const std::string& title) : title(title) {} // A
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {} // B
Bu her zaman aşağıdaki std::string
yollardan biriyle 1 oluşturur :
- Üye, üzerinden kopya oluşturulur
A
. - Üye, üzerinden inşa edilir
B
. - Üye,
std::string
aracılığıyla (muhtemelen dönüştüren) bir kurucu tarafından oluşturulurB
.
# 5 - Yalnızca bir yönlendirme kurucusu - kopyalama dönüştürme yapıcısını # 4'ten kaldırma.
template<typename... Args>
explicit MyClass(Args&&... args) : title(std::forward<Args>(args)...) {}
Bu her zaman std::string
# 4'te olduğu gibi 1 oluşturur , ancak hepsi iletme kurucusu aracılığıyla yapılır.
- Üye, kopya oluşturulmuştur.
- Üye taşınır.
- Üye,
std::string
(muhtemelen dönüştüren) bir kurucu tarafından oluşturulur.
# 6 - Dönüşüm kurucusunu ileten tek bir argüman.
template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}
Bu her zaman std::string
# 4 ve # 5'te olduğu gibi 1 oluşturacak , ancak yalnızca bir argüman alacak ve bunu kurucuya iletecektir std::string
.
- Üye, kopya oluşturulmuştur.
- Üye taşınır.
- Üye,
std::string
dönüştürücü bir kurucu tarafından oluşturulur.
Yapıcıda #6
birden fazla argüman almak istiyorsanız, mükemmel yönlendirme yapmak için seçenek kolayca kullanılabilir MyClass
. Diyelim ki bir int
üyeniz ve başka bir std::string
üyeniz var:
template<typename T, typename U>
MyClass(int X, T&& title, U&& title2) :
x(X),
title(std::forward<T>(title)),
title2(std::forward<U>(title2))
{}
Bir referansın kopyalanması, orijinal değişkenin bir kopyasını oluşturur (orijinal ve yenisi farklı alanlardadır), yerel değişken yayınlarını yerel değişkeninizin bir r değerine taşır (ve yine, orijinal ve yenisi farklı alanlardadır).
Derleyici bakış açısından, move
daha hızlı olabilir (ve öyle):
#include <string>
void MyClass(std::string title){
std::string title2 = std::move(title);
}
Çevirir:
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
Ancak,
void MyClass(std::string& title){
std::string title = title;
}
daha büyük bir kod üretir (GCC için benzer):
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"
Yani evet, std::move
daha iyi (bu koşullar altında).
Tamam bir const referansı kullanın, ardından üye başlatıcı listelerini kullanın:
MyClass(const std::string &title) : m_title{title}
M_title, sınıftaki üye dizenizdir.
Burada yararlı yardım bulabilirsiniz: Oluşturucu üye başlatıcı listeleri
: 2 durumlar vardır lvalue veya rvalue ait std::string
.
içinde std::string const&
versiyonunda, lvalue vaka verimli yeterince vardır başvuruyla geçirildi ve daha sonra kopyalanmış . ancak rvalue edilecektir kopyalanan yerine taşındı çok daha düşük verimlilikte olan.
içinde std::string
versiyonunda, lvalue olduğu geçtiğinde kopyalanan ve daha sonra hareket üyesine. rvalue edilecek iki kez taşındı bu durumda. ama genellikle ucuzdur , hareket oluşturucu.
üstelik, içinde std::string&&
sürümü, bir alamaz lvalue ama rvalue olan referans olarak geçti ve ardından taşınmış iyi iki kez taşındı daha.
çok açıktır ki, bu ikisi ile en iyi yöntem const&
ve &&
STL hep ne yaptığını gibi. ancak eğer hareket kurucu yeterince ucuzsa, sadece değere göre geçmek ve hareket etmek de kabul edilebilir.