Move-semantics เทียบกับการอ้างอิง const [ซ้ำกัน]

Aug 18 2020

คลาสของฉันมีตัวแปรสตริงและฉันต้องการเริ่มต้นด้วยค่าที่ส่งไปยังตัวสร้าง

ครูของฉันคิดว่าเราจะส่งสตริงเป็น 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 ++ สมัยใหม่คืออะไร

ฉันมองไปรอบ ๆ แล้ว แต่ไม่มีอะไรตอบคำถามของฉันได้เลย ขอบคุณล่วงหน้า!

คำตอบ

3 TedLyngmo Aug 18 2020 at 19:32

ไม่มีใครที่ดีที่สุดตั้งแต่พวกเขาทั้งสองสร้างเริ่มต้น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::stringlike ใน # 4 เสมอ แต่ทั้งหมดจะทำผ่านตัวสร้างการส่งต่อ

  • สมาชิกถูกสร้างสำเนา
  • สมาชิกถูกสร้างขึ้น
  • สมาชิกถูกสร้างโดยคอนสตรัคเตอร์std::string(อาจจะแปลง)

# 6 - ตัวสร้างการแปลงการส่งต่ออาร์กิวเมนต์เดียว

template<typename T>
explicit MyClass(T&& title) : title(std::forward<T>(title)) {}

สิ่งนี้จะสร้าง 1 std::stringlike ใน # 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))
{}
1 Jose Aug 18 2020 at 18:10

การคัดลอกการอ้างอิงจะสร้างสำเนาของตัวแปรดั้งเดิม (ตัวแปรดั้งเดิมและตัวแปรใหม่อยู่ในพื้นที่ที่ต่างกัน) การย้ายตัวแปรโลคัลไปยังค่าตัวแปรโลคัลของคุณ (และอีกครั้งเดิมและตัวแปรใหม่อยู่ในพื้นที่ที่ต่างกัน)

จากมุมมองของคอมไพเลอร์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จะดีกว่า (ภายใต้สถานการณ์เหล่านี้)

abraxas Aug 18 2020 at 18:19

ใช้การอ้างอิง const จากนั้นใช้รายการเริ่มต้นของสมาชิก:

MyClass(const std::string &title) : m_title{title}

โดยที่ m_title คือสตริงสมาชิกของคุณในชั้นเรียน

คุณสามารถค้นหาความช่วยเหลือที่เป็นประโยชน์ได้ที่นี่: รายชื่อผู้เริ่มต้นสมาชิกตัวสร้าง

RedFog Aug 18 2020 at 18:32

มี 2 ​​กรณี: lvalueหรือrvalueของstd::string.

ในstd::string const&รุ่นlvalueกรณีก็พอมีประสิทธิภาพผ่านอ้างอิงและคัดลอกแล้ว แต่ค่าrvalueจะถูกคัดลอกแทนที่จะย้ายซึ่งมีประสิทธิภาพต่ำกว่ามาก

ในstd::stringเวอร์ชันlvalueจะถูกคัดลอกเมื่อผ่านแล้วย้ายไปยังสมาชิก rvalueจะถูกย้ายสองครั้งในกรณีนี้ แต่โดยทั่วไปแล้วราคาถูกตัวสร้างการเคลื่อนย้าย

นอกจากนี้ในstd::string&&เวอร์ชันไม่สามารถรับค่าlvalueได้ แต่rvalueจะถูกส่งผ่านโดยการอ้างอิงแล้วย้ายดีกว่าย้ายสองครั้ง

เห็นได้ชัดว่าเป็นแนวทางปฏิบัติที่ดีที่สุดสำหรับทั้งสองอย่างconst&และ&&เช่นเดียวกับที่ STL ทำอยู่เสมอ แต่ถ้าตัวสร้างการเคลื่อนย้ายมีราคาถูกเพียงพอเพียงแค่ส่งผ่านค่าและการเคลื่อนย้ายก็เป็นที่ยอมรับ