Taşıma semantiği ve sabit referansı [yineleme]

Aug 18 2020

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

3 TedLyngmo Aug 18 2020 at 19:32

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::stringyollardan biriyle 1 veya 2 s oluşturacaktır :

  • Üye, kopya oluşturulmuştur.
  • A std::string, std::stringdö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::stringyollardan 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.str1str2
  • 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::stringdö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::stringyollardan biriyle 1 veya 2 s oluşturacaktır :

  • Üye, kopya oluşturulmuştur.
  • Üye taşınır.
  • A std::string, std::stringdönüştüren bir kurucu tarafından oluşturulur ve ardından üye taşınır.

Şimdiye kadar, seçenek #3en 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::stringyollardan biriyle 1 oluşturur :

  • Üye, üzerinden kopya oluşturulur A.
  • Üye, üzerinden inşa edilir B.
  • Üye, std::stringaracılığıyla (muhtemelen dönüştüren) bir kurucu tarafından oluşturulur B.

# 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::stringdönüştürücü bir kurucu tarafından oluşturulur.

Yapıcıda #6birden 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))
{}
1 Jose Aug 18 2020 at 18:10

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, movedaha 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::movedaha iyi (bu koşullar altında).

abraxas Aug 18 2020 at 18:19

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

RedFog Aug 18 2020 at 18:32

: 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::stringversiyonunda, 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.