Pindah-semantik vs referensi const [duplikat]

Aug 18 2020

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

3 TedLyngmo Aug 18 2020 at 19:32

Tidak ada yang optimal karena keduanya membangun default titleterlebih 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::stringdetik dengan salah satu cara berikut:

  • Anggota adalah salinan yang dibuat.
  • A std::stringdikonstruksi oleh konstruktor std::stringpengubah dan anggota dikonstruksikan.

# 2 - Konstruktor konversi tunggal yang mengambil std::stringnilai menurut.

MyClass(std::string title) : title(std::move(title)) {}

Ini akan membuat 1 atau 2 std::stringdetik 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::stringkonversi 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::stringdetik dengan salah satu cara berikut:

  • Anggota adalah salinan yang dibuat.
  • Anggota bergerak dibangun.
  • A std::stringdikonstruksi oleh konstruktor std::stringpengubah dan anggota dipindahkan.

Sejauh ini, opsi #3tampaknya 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::stringdengan salah satu cara berikut:

  • Anggota adalah salinan yang dibangun melalui A.
  • Anggota dipindahkan melalui B.
  • Anggota dibangun oleh std::string(mungkin mengubah) konstruktor melalui B.

# 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::stringseperti 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::stringseperti di # 4 dan # 5 tetapi hanya akan mengambil satu argumen dan meneruskannya ke std::stringkonstruktor.

  • Anggota adalah salinan yang dibuat.
  • Anggota bergerak dibangun.
  • Anggota dibangun oleh std::stringkonstruktor pengubah.

Opsi #6dapat dengan mudah digunakan untuk melakukan penerusan sempurna jika Anda ingin mengambil banyak argumen dalam MyClasskonstruktor. Misalkan Anda memiliki intanggota dan std::stringanggota 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))
{}
1 Jose Aug 18 2020 at 18:10

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).

abraxas Aug 18 2020 at 18:19

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

RedFog Aug 18 2020 at 18:32

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