เหตุใดจึงสามารถส่ง T * ในการลงทะเบียน แต่ unique_ptr <T> ไม่สามารถทำได้?


85

ฉันกำลังดูการสนทนาของ Chandler Carruth ใน CppCon 2019:

ไม่มี Abstractions Zero-Cost

ในนั้นเขาให้ตัวอย่างของวิธีการที่เขารู้สึกประหลาดใจเพียงเท่าใดค่าใช้จ่ายที่คุณเกิดขึ้นโดยใช้std::unique_ptr<int>มากกว่าint*; ส่วนนั้นเริ่มต้นที่เวลา 17:25 น

คุณสามารถดูผลการรวบรวมตัวอย่างของคู่ของเขา (godbolt.org) - เพื่อเป็นพยานว่าที่จริงดูเหมือนว่าคอมไพเลอร์ไม่เต็มใจที่จะผ่านค่า unique_ptr - ซึ่งในความเป็นจริงในบรรทัดล่างคือ เพียงที่อยู่ - ภายในการลงทะเบียนเท่านั้นในหน่วยความจำแบบตรง

หนึ่งในประเด็นที่นายคาร์รั ธ ทำเวลาประมาณ 27:00 น. คือ C ++ ABI ต้องการพารามิเตอร์ตามค่า (บางส่วน แต่ไม่ใช่ทั้งหมดบางทีอาจเป็น - ประเภทที่ไม่ใช่แบบดั้งเดิมหรือไม่ใช่ประเภทที่ไม่สามารถแก้ไขได้) จะถูกส่งผ่านในหน่วยความจำ มากกว่าภายในทะเบียน

คำถามของฉัน:

  1. นี่เป็นข้อกำหนดของ ABI ในบางแพลตฟอร์มหรือไม่ (อันไหน) หรือบางทีมันอาจเป็นเพียงการมองโลกในแง่ร้ายในบางสถานการณ์?
  2. เหตุใด ABI จึงเป็นเช่นนั้น นั่นคือถ้าเขตข้อมูลของโครงสร้าง / คลาสพอดีภายในรีจิสเตอร์หรือแม้แต่รีจิสเตอร์เดียว - ทำไมเราไม่สามารถส่งต่อได้ภายในรีจิสเตอร์นั้น?
  3. คณะกรรมการมาตรฐาน C ++ ได้กล่าวถึงประเด็นนี้ในปีที่ผ่านมาหรือไม่?

PS - เพื่อไม่ให้คำถามนี้โดยไม่มีรหัส:

ตัวชี้ธรรมดา:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

ตัวชี้ที่ไม่ซ้ำกัน:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}

8
ฉันไม่แน่ใจว่าข้อกำหนดของ ABI นั้นเป็นอย่างไร แต่ไม่ได้ห้ามวางโครงสร้างในทะเบียน
แฮโรลด์

6
ถ้าฉันต้องเดาฉันต้องบอกว่ามันเกี่ยวข้องกับฟังก์ชั่นที่ไม่สำคัญของสมาชิกที่ต้องการthisตัวชี้ที่ชี้ไปยังตำแหน่งที่ถูกต้อง unique_ptrมีเหล่านั้น การลงทะเบียนเพื่อจุดประสงค์นั้นจะเป็นการลบล้างการเพิ่มประสิทธิภาพ "pass in a register" ทั้งหมด
StoryTeller - Unslander Monica

2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls ดังนั้นพฤติกรรมนี้ต้องการ ทำไม? itanium-cxx-abi.github.io/cxx-abi/cxx-closed.htmlค้นหาปัญหา C-7 มีคำอธิบายอยู่ที่นั่น แต่ไม่ละเอียดเกินไป แต่ใช่พฤติกรรมนี้ไม่สมเหตุสมผลสำหรับฉัน วัตถุเหล่านี้สามารถส่งผ่านกองได้ตามปกติ การผลักพวกมันให้กองซ้อนและจากนั้นส่งผ่านการอ้างอิง (สำหรับวัตถุ "ที่ไม่สำคัญ") ดูจะเป็นการสิ้นเปลือง
geza

6
ดูเหมือนว่า C ++ กำลังละเมิดหลักการของตัวเองที่นี่ซึ่งค่อนข้างน่าเศร้า ฉันมั่นใจ 140% ที่ไม่เหมือนใครเพียงแค่หายไปหลังจากคอมไพล์ ท้ายที่สุดแล้วมันเป็นเพียงแค่การโทร destructor ที่เลื่อนออกไปซึ่งเป็นที่รู้จักกันในเวลารวบรวม
One Monkey Monkey Squad

7
@ MaximEgorushkin: ถ้าคุณเขียนด้วยมือคุณจะต้องวางตัวชี้ไว้ในรีจิสเตอร์ไม่ใช่ในสแต็ก
einpoklum

คำตอบ:


49
  1. นี่เป็นข้อกำหนดของ ABI จริง ๆ หรืออาจเป็นเพียงการมองโลกในแง่ร้ายในบางสถานการณ์

ตัวอย่างหนึ่งคือระบบวี Application Binary Interface AMD64 สถาปัตยกรรมเสริมประมวลผล ABI นี้ใช้สำหรับซีพียูที่รองรับ 64-bit x86 (Linux x86_64 architecure) มีการติดตามบน Solaris, Linux, FreeBSD, macOS, Windows Subsystem สำหรับ Linux:

ถ้าวัตถุ C ++ มีตัวสร้างการคัดลอกที่ไม่สำคัญหรือ destructor ที่ไม่สำคัญก็จะถูกส่งผ่านโดยการอ้างอิงที่มองไม่เห็น (วัตถุถูกแทนที่ในรายการพารามิเตอร์โดยตัวชี้ที่มีคลาส INTEGER)

วัตถุที่มีตัวสร้างการคัดลอกที่ไม่สำคัญหรือ destructor ที่ไม่สำคัญไม่สามารถส่งผ่านโดยค่าได้เนื่องจากวัตถุดังกล่าวจะต้องมีที่อยู่ที่กำหนดไว้อย่างดี ปัญหาที่คล้ายกันใช้เมื่อส่งคืนวัตถุจากฟังก์ชัน

โปรดทราบว่ามีการลงทะเบียนวัตถุประสงค์ทั่วไปเพียง 2 ข้อเท่านั้นที่สามารถใช้สำหรับการส่ง 1 วัตถุที่มีตัวสร้างการคัดลอกเล็กน้อยและ destructor เล็กน้อยเช่นค่าของวัตถุที่มีsizeofค่าไม่เกิน 16 เท่านั้นที่สามารถส่งผ่านการลงทะเบียนได้ ดูการประชุมทางโทรศัพท์โดย Agner Fogเพื่อรับการรักษาโดยละเอียดเกี่ยวกับการประชุมทางโทรศัพท์โดยเฉพาะอย่างยิ่ง§7.1การผ่านและการส่งคืนวัตถุ มีแบบแผนการโทรแยกต่างหากสำหรับการส่งประเภท SIMD ในการลงทะเบียน

มี ABIs ที่แตกต่างกันสำหรับสถาปัตยกรรม CPU อื่น ๆ


  1. เหตุใด ABI จึงเป็นเช่นนั้น นั่นคือถ้าเขตข้อมูลของโครงสร้าง / คลาสพอดีภายในรีจิสเตอร์หรือแม้แต่รีจิสเตอร์เดียว - ทำไมเราไม่สามารถส่งต่อได้ภายในรีจิสเตอร์นั้น?

เป็นรายละเอียดการนำไปใช้ แต่เมื่อจัดการข้อยกเว้นระหว่างการคลี่คลายวัตถุที่มีระยะเวลาการจัดเก็บอัตโนมัติที่ถูกทำลายจะต้องสามารถกำหนดแอดเดรสได้เทียบกับกรอบสแต็กฟังก์ชั่นเนื่องจากการลงทะเบียนถูกปิดกั้นในเวลานั้น สแต็กรหัสคลี่คลายต้องการที่อยู่ของวัตถุเพื่อเรียกใช้ destructors ของพวกเขา แต่วัตถุในการลงทะเบียนไม่มีที่อยู่

อวดรู้destructors ทำงานบนวัตถุ :

วัตถุครอบครองพื้นที่เก็บข้อมูลในช่วงเวลาของการก่อสร้าง ([class.cdtor]) ตลอดอายุการใช้งานและในช่วงเวลาที่ถูกทำลาย

และวัตถุที่ไม่สามารถอยู่ใน C ++ หากไม่มีแอดเดรสจัดเก็บข้อมูลจะถูกจัดสรรให้มันเพราะตัวตนของวัตถุที่อยู่ของมัน

เมื่อที่อยู่ของวัตถุที่มีตัวสร้างการคัดลอกเล็กน้อยที่เก็บไว้ในการลงทะเบียนจำเป็นต้องใช้คอมไพเลอร์สามารถเก็บวัตถุไว้ในหน่วยความจำและรับที่อยู่ หากตัวสร้างการคัดลอกนั้นไม่น่าสนใจในอีกทางหนึ่งคอมไพเลอร์ไม่สามารถเก็บมันไว้ในหน่วยความจำได้มันค่อนข้างจำเป็นต้องเรียกตัวสร้างสำเนาซึ่งใช้การอ้างอิงและด้วยเหตุนี้จึงต้องใช้ที่อยู่ของวัตถุในรีจิสเตอร์ แบบแผนการเรียกอาจไม่สามารถขึ้นอยู่กับว่าตัวสร้างการคัดลอกถูก inline ใน callee หรือไม่

อีกวิธีในการคิดเกี่ยวกับสิ่งนี้คือสำหรับประเภทที่ทำสำเนาได้เล็กน้อยคอมไพเลอร์จะถ่ายโอนค่าของวัตถุในรีจิสเตอร์ซึ่งวัตถุสามารถกู้คืนได้โดยหน่วยความจำธรรมดาถ้าจำเป็น เช่น:

void f(long*);
void g(long a) { f(&a); }

บน x86_64 ด้วย System V ABI รวบรวมเป็น:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

ในการพูดคุยที่กระตุ้นความคิดของเขา Chandler Carruth กล่าวว่าการเปลี่ยนแปลง ABI ที่แตกหักอาจจำเป็น IMO การเปลี่ยนแปลง ABI อาจไม่ทำลายหากฟังก์ชันที่ใช้ ABI ใหม่เลือกที่จะเชื่อมโยงใหม่อย่างชัดเจนเช่นประกาศในextern "C++20" {}บล็อก (อาจเป็นในอินไลน์เนมสเปซใหม่สำหรับการย้าย API ที่มีอยู่) ดังนั้นเฉพาะโค้ดที่รวบรวมกับการประกาศฟังก์ชันใหม่ด้วยการเชื่อมโยงใหม่เท่านั้นที่สามารถใช้ ABI ใหม่ได้

โปรดทราบว่า ABI ใช้ไม่ได้เมื่อฟังก์ชั่นที่เรียกได้รับการ inlined เช่นเดียวกับการสร้างรหัสลิงก์เวลาคอมไพเลอร์สามารถฟังก์ชั่นอินไลน์ที่กำหนดไว้ในหน่วยการแปลอื่น ๆ หรือใช้แบบแผนการโทรที่กำหนดเอง


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

8

ด้วย ABIs ทั่วไปตัวทำลายล้างที่ไม่สำคัญ -> ไม่สามารถผ่านการลงทะเบียนได้

(ภาพประกอบของจุดในคำตอบของ @ MaximEgorushkin โดยใช้ตัวอย่างของ @ harold ในความคิดเห็น; แก้ไขตามความคิดเห็นของ @ Yakk)

หากคุณรวบรวม:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

คุณได้รับ:

test(Foo):
        mov     eax, edi
        ret

เช่นFooวัตถุถูกส่งผ่านไปยังtestregister ( edi) และส่งคืนใน register ( eax)

เมื่อ destructor นั้นไม่สำคัญ (เช่นstd::unique_ptrตัวอย่างของ OP) - ABI ทั่วไปต้องการการวางบนสแต็ก สิ่งนี้เป็นจริงแม้ว่าผู้ทำลายจะไม่ใช้ที่อยู่ของวัตถุเลย

ดังนั้นแม้ในกรณีที่รุนแรงที่สุดของตัวทำลายสิ่งที่ไม่ทำอะไรเลยถ้าคุณรวบรวม:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

คุณได้รับ:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

ด้วยการโหลดและจัดเก็บที่ไร้ประโยชน์


ฉันไม่เชื่อเรื่องนี้ destructor ที่ไม่น่าสนใจไม่ได้ทำอะไรเลยที่จะห้ามกฎ as-if หากไม่ได้สังเกตที่อยู่จะไม่มีเหตุผลใดที่จะต้องมีที่อยู่นี้ ดังนั้นเรียบเรียงสอดคล้องอย่างมีความสุขสามารถวางไว้ในการลงทะเบียนถ้าทำเช่นนั้นไม่ได้เปลี่ยนพฤติกรรมที่สังเกตได้ (และคอมไพเลอร์ในปัจจุบันจะในความเป็นจริงจะทำเช่นนั้นถ้าสายที่เป็นที่รู้จักกัน )
ComicSansMS

1
น่าเสียดายที่เป็นวิธีอื่น (ฉันยอมรับว่าบางสิ่งนี้เกินกว่าเหตุผลแล้ว) เพื่อความแม่นยำ: ฉันไม่เชื่อว่าเหตุผลที่คุณให้ไว้จะทำให้ ABI ที่เป็นไปได้ที่อนุญาตให้ผ่านปัจจุบันstd::unique_ptrในการลงทะเบียนที่ไม่สอดคล้อง
ComicSansMS

3
"destructor เล็กน้อย [การอ้างอิงที่จำเป็น]" ชัดเจนอย่างชัดเจน; ถ้ารหัสไม่จริงขึ้นอยู่กับที่อยู่แล้วเป็นถ้าหมายถึงความต้องการที่อยู่ไม่อยู่บนเครื่องที่เกิดขึ้นจริง ที่อยู่จะต้องมีอยู่ในเครื่องที่เป็นนามธรรมแต่สิ่งที่อยู่ในเครื่องที่เป็นนามธรรมที่ไม่มีผลกระทบกับเครื่องที่แท้จริงคือสิ่งที่ได้รับอนุญาตให้กำจัด
Yakk - Adam Nevraumont

2
@einpoklum ไม่มีอะไรในมาตรฐานที่ระบุว่ามีการลงทะเบียนอยู่ คำหลักการลงทะเบียนเพียงระบุว่า "คุณไม่สามารถรับที่อยู่" มีเพียงเครื่องนามธรรมเท่ามาตรฐานที่เกี่ยวข้อง "ราวกับว่า" หมายความว่าการใช้งานเครื่องจริงใด ๆ ต้องการเพียงการทำงาน "ราวกับว่า" เครื่องนามธรรมทำงานขึ้นอยู่กับพฤติกรรมที่ไม่ได้กำหนดโดยมาตรฐาน ขณะนี้มีปัญหาที่ท้าทายมากเกี่ยวกับการมีวัตถุในการลงทะเบียนซึ่งทุกคนได้พูดคุยกันอย่างกว้างขวาง นอกจากนี้การเรียกประชุมซึ่งมาตรฐานยังไม่หารือมีความต้องการในทางปฏิบัติ
Yakk - Adam Nevraumont

1
@ einpoklum ไม่ในเครื่องนามธรรมทุกสิ่งมีที่อยู่ แต่ที่อยู่สามารถสังเกตได้ในบางสถานการณ์เท่านั้น registerคำหลักก็ตั้งใจที่จะทำให้มันน่ารำคาญสำหรับเครื่องทางกายภาพในการจัดเก็บบางสิ่งบางอย่างในการลงทะเบียนโดยการปิดกั้นสิ่งที่จริงทำให้มันยากขึ้นที่จะ "มีที่อยู่ไม่" ในเครื่องกายภาพ
Yakk - Adam Nevraumont

2

นี่เป็นข้อกำหนดของ ABI ในบางแพลตฟอร์มหรือไม่ (อันไหน) หรือบางทีมันอาจเป็นเพียงการมองโลกในแง่ร้ายในบางสถานการณ์?

หากมีสิ่งใดปรากฎที่ขอบเขตหน่วยการประนีประนอมแล้วไม่ว่าจะถูกกำหนดโดยปริยายหรือโดยชัดแจ้งก็จะกลายเป็นส่วนหนึ่งของ ABI

เหตุใด ABI จึงเป็นเช่นนั้น

ปัญหาพื้นฐานคือการลงทะเบียนจะได้รับการบันทึกและเรียกคืนตลอดเวลาเมื่อคุณเลื่อนขึ้นและลงของ call stack ดังนั้นจึงไม่มีประโยชน์ที่จะมีการอ้างอิงหรือตัวชี้ไปที่พวกเขา

การจัดเรียงและการปรับให้เหมาะสมที่เกิดขึ้นนั้นเป็นสิ่งที่ดีเมื่อมันเกิดขึ้น แต่นักออกแบบ ABI ไม่สามารถวางใจได้ว่ามันจะเกิดขึ้น พวกเขาต้องออกแบบ ABI โดยถือว่าเป็นกรณีที่เลวร้ายที่สุด ฉันไม่คิดว่าโปรแกรมเมอร์จะมีความสุขมากกับคอมไพเลอร์ที่เปลี่ยน ABI ขึ้นอยู่กับระดับการปรับให้เหมาะสม

ชนิดที่คัดลอกได้เล็กน้อยสามารถส่งผ่านในรีจิสเตอร์ได้เนื่องจากการดำเนินการคัดลอกเชิงตรรกะสามารถแบ่งออกเป็นสองส่วน พารามิเตอร์จะถูกคัดลอกไปยังรีจิสเตอร์ที่ใช้สำหรับส่งพารามิเตอร์โดยผู้เรียกจากนั้นคัดลอกไปยังตัวแปรโลคัลโดย callee ไม่ว่าตัวแปรโลคัลมีตำแหน่งหน่วยความจำหรือไม่นั้นเป็นเพียงข้อกังวลของผู้ติดตาม

ชนิดที่ตัวคัดลอกหรือย้ายตัวสร้างต้องใช้ในอีกด้านหนึ่งไม่สามารถแยกการดำเนินการคัดลอกได้ด้วยวิธีนี้ดังนั้นจึงต้องส่งผ่านในหน่วยความจำ

คณะกรรมการมาตรฐาน C ++ ได้กล่าวถึงประเด็นนี้ในปีที่ผ่านมาหรือไม่?

ฉันไม่มีความคิดหากหน่วยงานมาตรฐานได้พิจารณาเรื่องนี้

วิธีแก้ปัญหาที่ชัดเจนสำหรับฉันคือการเพิ่มการเคลื่อนไหวแบบทำลายล้างที่เหมาะสม (แทนที่จะเป็นครึ่งทางในปัจจุบันของ "สถานะที่ถูกต้อง แต่ไม่ได้ระบุไว้เป็นอย่างอื่น") ไปยังไฟล์แนบจากนั้นแนะนำวิธีตั้งค่าสถานะ "แม้ว่าจะไม่อนุญาตให้ทำสำเนาเล็กน้อย

แต่โซลูชันดังกล่าว WOULD ต้องการทำลาย ABI ของรหัสที่มีอยู่เพื่อนำไปใช้กับประเภทที่มีอยู่ซึ่งอาจนำมาซึ่งความต้านทานที่ค่อนข้างดี (แม้ว่า ABI จะแตกเนื่องจากผลของ C ++ รุ่นมาตรฐานใหม่ที่ไม่เคยมีมาก่อนเช่นการเปลี่ยนแปลงสตริง ใน C ++ 11 ส่งผลให้ ABI break ..


คุณสามารถอธิบายรายละเอียดเกี่ยวกับการย้ายการทำลายล้างที่เหมาะสมที่จะอนุญาตให้มีการส่งผ่าน unique_ptr ในการลงทะเบียนหรือไม่? นั่นอาจเป็นเพราะจะทำให้ความต้องการในการจัดเก็บที่อยู่ลดลง?
einpoklum

การเคลื่อนย้ายการทำลายล้างที่เหมาะสมจะช่วยให้แนวคิดของการเคลื่อนไหวทำลายล้างที่จะแนะนำ สิ่งนี้จะช่วยให้การเคลื่อนไหวเล็ก ๆ น้อย ๆ ของ ABI แตกต่างกันไปในลักษณะเดียวกันกับที่สำเนาเล็ก ๆ น้อย ๆ สามารถเป็นได้ในทุกวันนี้
plugwash

แม้ว่าคุณจะต้องการเพิ่มกฎที่คอมไพเลอร์สามารถใช้การส่งผ่านพารามิเตอร์เป็นการย้ายปกติหรือคัดลอกแล้วตามด้วย "การย้ายทำลายแบบไม่สำคัญ" เพื่อให้แน่ใจว่าเป็นไปได้เสมอที่จะผ่านการลงทะเบียนไม่ว่าพารามิเตอร์นั้นมาจากไหน
plugwash

เพราะขนาดรีจิสเตอร์สามารถเก็บตัวชี้ไว้ได้ แต่มีโครงสร้างที่เป็นเอกลักษณ์หรือไม่ ขนาดของอะไร (unique_ptr <T>)?
Mel Viso Martinez

@MelVisoMartinez คุณอาจสับสนunique_ptrและซีแมนทิกส์shared_ptr: shared_ptr<T>ให้คุณมอบ ctor 1) ptr x ไปยังอ็อบเจกต์ U ที่จะถูกลบด้วยสแตติกประเภท U w / นิพจน์delete x;(ดังนั้นคุณไม่จำเป็นต้องมี dtor เสมือนจริง) 2) หรือ แม้กระทั่งฟังก์ชั่นการล้างข้อมูลที่กำหนดเอง ซึ่งหมายความว่าสถานะรันไทม์ถูกใช้ภายในshared_ptrบล็อกควบคุมเพื่อเข้ารหัสข้อมูลนั้น OTOH unique_ptrไม่มีฟังก์ชั่นดังกล่าวและไม่เข้ารหัสพฤติกรรมการลบในสถานะ วิธีเดียวในการปรับแต่งการล้างข้อมูลคือการสร้างเทมเพลต instanciation อื่น (ประเภทคลาสอื่น)
curiousguy

-1

ก่อนอื่นเราต้องกลับไปที่ความหมายของการส่งผ่านมูลค่าและโดยการอ้างอิง

สำหรับภาษาอย่าง Java และ SML การส่งผ่านค่าจะตรงไปตรงมา (และไม่มีการอ้างอิงผ่าน) เช่นเดียวกับการคัดลอกค่าตัวแปรเนื่องจากตัวแปรทั้งหมดเป็นเพียงสเกลาร์และมีความหมายในการคัดลอกในตัว พิมพ์ใน C ++ หรือ "การอ้างอิง" (พอยน์เตอร์ที่มีชื่อและไวยากรณ์ต่างกัน)

ใน C เรามีสเกลาร์และประเภทที่ผู้ใช้กำหนด:

  • สเกลาร์มีค่าตัวเลขหรือนามธรรม (ตัวชี้ไม่ใช่ตัวเลข แต่มีค่านามธรรม) ที่คัดลอก
  • ประเภทการรวมมีสมาชิกเริ่มต้นที่อาจถูกคัดลอกทั้งหมด:
    • สำหรับประเภทผลิตภัณฑ์ (อาร์เรย์และโครงสร้าง): เรียกซ้ำสมาชิกทั้งหมดของโครงสร้างและองค์ประกอบของอาร์เรย์จะถูกคัดลอก (ไวยากรณ์ของฟังก์ชั่น C ไม่สามารถส่งผ่านอาร์เรย์ได้ด้วยมูลค่าโดยตรงเฉพาะสมาชิกอาร์เรย์ของ struct แต่นั่นเป็นรายละเอียด )
    • สำหรับประเภทรวม (สหภาพ): ค่าของ "สมาชิกที่ใช้งาน" จะถูกเก็บไว้; เห็นได้ชัดว่าการคัดลอกสมาชิกโดยสมาชิกไม่ได้เป็นไปตามลำดับเนื่องจากไม่สามารถเริ่มสมาชิกทั้งหมดได้

ในประเภท C ++ ที่ผู้ใช้กำหนดสามารถมีความหมายของการคัดลอกที่ผู้ใช้กำหนดซึ่งช่วยให้การเขียนโปรแกรม "เชิงวัตถุ" อย่างแท้จริงด้วยวัตถุที่เป็นเจ้าของทรัพยากรและการดำเนินการ "คัดลอกลึก" ในกรณีเช่นนี้การทำสำเนาเป็นการเรียกฟังก์ชั่นที่เกือบจะสามารถดำเนินการตามอำเภอใจได้

สำหรับ C structs ที่คอมไพล์เป็น C ++ "การคัดลอก" ยังคงถูกกำหนดเป็นการเรียกการดำเนินการคัดลอกที่ผู้ใช้กำหนด มันหมายความว่าความหมายของโปรแกรมย่อยทั่วไป C / C ++ แตกต่างกันใน C และ C ++: ใน C ประเภทรวมทั้งหมดจะถูกคัดลอกใน C ++ ฟังก์ชั่นการคัดลอกที่สร้างขึ้นโดยปริยายจะถูกเรียกให้คัดลอกสมาชิกแต่ละคน ผลลัพธ์ที่ได้คือว่าในกรณีใดกรณีหนึ่งสมาชิกแต่ละคนจะถูกคัดลอก

(มีข้อยกเว้นฉันคิดว่าเมื่อคัดลอกโครงสร้างภายในสหภาพ)

ดังนั้นสำหรับประเภทคลาสวิธีเดียว (การคัดลอกภายนอกสหภาพ) เพื่อสร้างอินสแตนซ์ใหม่คือผ่านตัวสร้าง (แม้สำหรับผู้ที่สร้างคอมไพเลอร์เล็กน้อยที่สร้างคอนสตรัคเตอร์)

คุณไม่สามารถใช้ที่อยู่ของ rvalue ผ่านโอเปอเรเตอร์ unary &แต่นั่นไม่ได้หมายความว่าไม่มีวัตถุ rvalue และวัตถุตามคำนิยามมีที่อยู่ ; และที่อยู่นั้นยังแสดงด้วยโครงสร้างไวยากรณ์: วัตถุประเภทคลาสสามารถสร้างได้โดยตัวสร้างเท่านั้นและมีthisตัวชี้ แต่สำหรับประเภทเล็ก ๆ น้อย ๆ ไม่มีคอนสตรัคเตอร์ที่ผู้ใช้เขียนดังนั้นไม่มีสถานที่ที่จะวางthisจนกระทั่งหลังจากที่สำเนาถูกสร้างและตั้งชื่อ

สำหรับประเภทสเกลาร์ค่าของวัตถุคือ rvalue ของวัตถุซึ่งเป็นค่าทางคณิตศาสตร์ล้วนที่เก็บไว้ในวัตถุ

สำหรับประเภทของคลาสความคิดเพียงอย่างเดียวของค่าของวัตถุก็คืออีกสำเนาของวัตถุซึ่งสามารถสร้างได้โดยตัวสร้างสำเนาซึ่งเป็นฟังก์ชันที่แท้จริง สร้างโดยไม่เรียกตัวสร้าง) นั่นหมายความว่ามูลค่าของวัตถุที่เป็นผลมาจากการเปลี่ยนแปลงของรัฐโปรแกรมทั่วโลกโดยการประหาร มันไม่สามารถเข้าถึงทางคณิตศาสตร์ได้

ดังนั้นผ่านค่าจริงๆไม่ได้เป็นสิ่ง: มันผ่านสำเนาโทรคอนสตรัคซึ่งน้อยสวย ตัวสร้างสำเนาคาดว่าจะดำเนินการ "คัดลอก" ที่เหมาะสมตามความหมายที่เหมาะสมของประเภทวัตถุโดยเคารพค่าคงที่ภายใน (ซึ่งเป็นคุณสมบัติผู้ใช้ที่เป็นนามธรรมไม่ใช่คุณสมบัติ C ++ ภายใน)

ผ่านค่าของวัตถุคลาสหมายความว่า:

  • สร้างอีกตัวอย่าง
  • จากนั้นให้ฟังก์ชันที่เรียกใช้กระทำกับอินสแตนซ์นั้น

โปรดทราบว่าปัญหาไม่มีอะไรเกี่ยวข้องกับการคัดลอกตัวเองเป็นวัตถุที่มีที่อยู่: พารามิเตอร์ฟังก์ชั่นทั้งหมดเป็นวัตถุและมีที่อยู่ (ที่ระดับความหมายของภาษา)

ปัญหาคือว่า:

  • การคัดลอกเป็นวัตถุใหม่ที่เริ่มต้นด้วยค่าทางคณิตศาสตร์ที่บริสุทธิ์ (ค่าความบริสุทธิ์ที่แท้จริง) ของวัตถุดั้งเดิมเช่นเดียวกับสเกลาร์
  • หรือสำเนาเป็นค่าของวัตถุต้นฉบับเช่นเดียวกับคลาส

ในกรณีของประเภทคลาสที่ไม่สำคัญคุณยังสามารถกำหนดสมาชิกของสมาชิกสำเนาของต้นฉบับได้ดังนั้นคุณจะได้กำหนดค่า rvalue ที่แท้จริงของต้นฉบับเนื่องจากความสำคัญของการดำเนินการคัดลอก (ตัวสร้างสำเนาและการมอบหมาย) ไม่เช่นนั้นด้วยฟังก์ชั่นผู้ใช้พิเศษโดยพลการ: ค่าของต้นฉบับจะต้องเป็นสำเนาที่สร้างขึ้น

วัตถุระดับจะต้องสร้างโดยผู้โทร; ตัวสร้างอย่างเป็นทางการมีthisตัวชี้ แต่เป็นพิธีไม่เกี่ยวข้องที่นี่: วัตถุทั้งหมดอย่างเป็นทางการมีที่อยู่ แต่เฉพาะผู้ที่ได้รับจริงที่อยู่ของพวกเขาใช้ในรูปแบบที่ไม่ใช่ท้องถิ่นอย่างหมดจด (เหมือน*&i = 1;ซึ่งคือการใช้ในท้องถิ่นอย่างหมดจดของที่อยู่) จำเป็นที่จะต้องมีการกำหนดไว้อย่างดี ที่อยู่

วัตถุจะต้องผ่านที่อยู่อย่างสมบูรณ์หากปรากฏว่ามีที่อยู่ในทั้งสองฟังก์ชันที่รวบรวมแยกต่างหาก:

void callee(int &i) {
  something(&i);
}

void caller() {
  int i;
  callee(i);
  something(&i);
}

ที่นี่แม้ว่าsomething(address)จะเป็นฟังก์ชั่นบริสุทธิ์หรือมาโครหรืออะไรก็ตาม (เช่นprintf("%p",arg)) ที่ไม่สามารถจัดเก็บที่อยู่หรือสื่อสารกับหน่วยงานอื่นเรามีความต้องการที่จะผ่านที่อยู่เพราะที่อยู่จะต้องกำหนดไว้อย่างดีสำหรับวัตถุintที่ไม่ซ้ำกัน เอกลักษณ์

เราไม่ทราบว่าฟังก์ชั่นภายนอกจะเป็น "บริสุทธิ์" ในแง่ของที่อยู่ที่ส่งไปยังมันหรือไม่

ที่นี่มีศักยภาพสำหรับการใช้ที่อยู่ที่แท้จริงทั้งในตัวสร้างที่ไม่สำคัญหรือ destructor ทางด้านผู้โทรอาจเป็นเหตุผลของการใช้เส้นทางที่ปลอดภัยเรียบง่ายและให้วัตถุในตัวตนของผู้โทรและผ่านที่อยู่ของมัน ตรวจสอบให้แน่ใจว่ามีการใช้ที่อยู่ที่ไม่ใช่เรื่องไร้สาระในตัวสร้างหลังจากการก่อสร้างและใน destructor ที่สอดคล้องกัน: thisต้องปรากฏเป็นเหมือนกันกับการมีอยู่ของวัตถุ

Constructor ที่ไม่สำคัญหรือ destructor เช่นเดียวกับฟังก์ชั่นอื่น ๆ สามารถใช้thisตัวชี้ในลักษณะที่ต้องการความสม่ำเสมอมากกว่าค่าของมันแม้ว่าวัตถุบางตัวที่มีสิ่งที่ไม่น่าสนใจอาจไม่:

struct file_handler { // don't use that class!
    file_handler () { this->fileno = -1; }
    file_handler (int f) { this->fileno = f; }
    file_handler (const file_handler& rhs) {
        if (this->fileno != -1)
            this->fileno = dup(rhs.fileno);
        else
            this->fileno = -1;
    }
    ~file_handler () {
        if (this->fileno != -1)
            close(this->fileno); 
    }
    file_handler &operator= (const file_handler& rhs);
};

โปรดทราบว่าในกรณีนั้นแม้จะมีการใช้ตัวชี้อย่างชัดเจน (ไวยากรณ์ชัดเจนthis->) ตัวตนของวัตถุนั้นไม่เกี่ยวข้อง: คอมไพเลอร์สามารถใช้การคัดลอกวัตถุในระดับบิตเพื่อย้ายและทำ "คัดลอก elision" นี้จะขึ้นอยู่กับระดับของ "ความบริสุทธิ์" ของการใช้งานthisในฟังก์ชั่นสมาชิกพิเศษ (ที่อยู่ไม่หนี)

แต่ความบริสุทธิ์ไม่ได้เป็นแอตทริบิวต์ที่มีอยู่ในระดับประกาศมาตรฐาน (ส่วนขยายของคอมไพเลอร์อยู่ที่คำอธิบายเพิ่มความบริสุทธิ์ไม่ใช่การประกาศฟังก์ชันอินไลน์) ดังนั้นคุณจะไม่สามารถกำหนด ABI ขึ้นอยู่กับความบริสุทธิ์ของรหัสที่อาจไม่สามารถใช้ได้ (รหัสอาจจะหรือ อาจไม่อินไลน์และพร้อมสำหรับการวิเคราะห์)

ความบริสุทธิ์นั้นวัดได้ว่าเป็น "บริสุทธิ์อย่างแน่นอน" หรือ "ไม่บริสุทธิ์หรือไม่ทราบ" พื้นดินทั่วไปหรือขอบเขตสูงสุดของความหมาย (สูงสุดจริง) หรือ LCM (ตัวคูณร่วมน้อย) คือ "ไม่ทราบ" ดังนั้น ABI จึงไม่ทราบ

สรุป:

  • การสร้างบางอย่างต้องการคอมไพเลอร์เพื่อกำหนดเอกลักษณ์ของวัตถุ
  • ABI ถูกกำหนดในรูปแบบของคลาสของโปรแกรมและไม่ใช่กรณีเฉพาะที่อาจเหมาะสมที่สุด

การทำงานในอนาคตที่เป็นไปได้:

หมายเหตุประกอบความบริสุทธิ์มีประโยชน์เพียงพอที่จะทำให้เป็นมาตรฐานและเป็นมาตรฐานหรือไม่


1
ตัวอย่างแรกของคุณดูเหมือนจะทำให้เข้าใจผิด ฉันคิดว่าคุณกำลังทำประเด็นโดยทั่วไป แต่ในตอนแรกฉันคิดว่าคุณกำลังทำการเปรียบเทียบกับรหัสในคำถาม แต่void foo(unique_ptr<int> ptr)ต้องใช้วัตถุชั้นโดยค่า วัตถุนั้นมีสมาชิกตัวชี้ แต่เรากำลังพูดถึงตัววัตถุคลาสที่ถูกส่งผ่านโดยการอ้างอิง (เพราะมันไม่ได้นิด-copyable จึงจำเป็นคอนสตรัค / destructor สอดคล้องthis.) นั่นคืออาร์กิวเมนต์ที่จริงและไม่ได้เชื่อมต่อกับตัวอย่างแรกของการส่งผ่านโดยการอ้างอิงอย่างชัดเจน ; ในกรณีนั้นตัวชี้จะถูกส่งผ่านในรีจิสเตอร์
Peter Cordes

@ PeterCordes " คุณกำลังทำการเปรียบเทียบกับรหัสในคำถาม " ฉันทำอย่างนั้น " คลาสวัตถุตามค่า " ใช่ฉันอาจจะอธิบายได้ว่าโดยทั่วไปแล้วไม่มีสิ่งเช่น "ค่า" ของคลาสวัตถุดังนั้นโดยค่าสำหรับประเภทคณิตศาสตร์ที่ไม่ใช่ไม่ใช่ "ตามค่า" " วัตถุนั้นมีสมาชิกตัวชี้ " ลักษณะเหมือน ptr ของ "smart ptr" นั้นไม่เกี่ยวข้อง และเป็นสมาชิก ptr ของ "smart ptr" PTR เป็นเพียงเซนต์คิตส์และเนวิสเหมือนint: ฉันเขียนตัวอย่าง "smart fileno" ซึ่งแสดงให้เห็นว่า "ความเป็นเจ้าของ" ไม่มีอะไรเกี่ยวข้องกับ "การถือ PTR"
curiousguy

1
ค่าของคลาสวัตถุคือการแสดงวัตถุ สำหรับunique_ptr<T*>นี่คือขนาดและเลย์เอาต์ที่เหมือนกันT*และเหมาะกับการลงทะเบียน อ็อบเจ็กต์คลาสที่สามารถคัดลอกได้เล็กน้อยสามารถส่งค่าตามค่าในรีจิสเตอร์ใน x86-64 System V เช่นเดียวกับการเรียกส่วนใหญ่ สิ่งนี้ทำสำเนาของunique_ptrวัตถุซึ่งแตกต่างจากintตัวอย่างของคุณโดยที่ callee &i คือที่อยู่ของผู้โทรiเพราะคุณส่งต่อโดยอ้างอิงที่ระดับ C ++ไม่ใช่แค่รายละเอียดการใช้ asm
Peter Cordes

1
เอ่อแก้ไขความคิดเห็นล่าสุดของฉัน มันไม่ใช่แค่การทำสำเนาของunique_ptrวัตถุ มันใช้std::moveจึงมีความปลอดภัยในการคัดลอกนั้นเพราะเห็นว่าจะไม่ส่งผลใน 2 unique_ptrฉบับเดียวกัน แต่สำหรับประเภทที่คัดลอกได้เล็กน้อยใช่มันจะคัดลอกวัตถุรวมทั้งหมด ถ้านั่นเป็นสมาชิกคนเดียวการประชุมที่ดีจะถือว่ามันเหมือนกับสเกลาร์ประเภทนั้น
Peter Cordes

1
ดูดีขึ้น หมายเหตุ: สำหรับ C structs ที่คอมไพล์เป็น C ++ - นี่ไม่ใช่วิธีที่มีประโยชน์ในการแนะนำความแตกต่างระหว่าง C ++ ใน C ++ struct{}คือโครงสร้าง C ++ บางทีคุณควรพูดว่า "โครงสร้างธรรมดา" หรือ "ไม่เหมือนกับ C" เพราะใช่มีความแตกต่าง ถ้าคุณใช้atomic_intเป็นสมาชิก struct C จะไม่คัดลอกข้อผิดพลาด C ++ บนตัวสร้างสำเนาที่ถูกลบ ฉันลืมว่า C ++ ทำอะไรกับโครงสร้างกับvolatileสมาชิกได้บ้าง C จะอนุญาตให้คุณทำstruct tmp = volatile_struct;สำเนาทั้งหมด (มีประโยชน์สำหรับ SeqLock); C ++ จะไม่
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.