มันโอเคที่จะส่งคืนค่าอาร์กิวเมนต์เริ่มต้นโดยการอ้างอิง const หรือไม่


26

มันโอเคที่จะส่งคืนค่าอาร์กิวเมนต์เริ่มต้นโดยการอ้างอิง const เช่นในตัวอย่างด้านล่าง:

https://coliru.stacked-crooked.com/a/ff76e060a007723b

#include <string>

const std::string& foo(const std::string& s = std::string(""))
{
    return s;
}

int main()
{
    const std::string& s1 = foo();
    std::string s2 = foo();

    const std::string& s3 = foo("s");
    std::string s4 = foo("s");
}

3
ทดสอบอย่างง่าย: แทนที่std::stringด้วยคลาสของคุณเองเพื่อให้คุณสามารถติดตามการก่อสร้างและการทำลายล้าง
user4581301

1
@ user4581301 ถ้าลำดับถูกต้องจะไม่พิสูจน์ว่าการสร้างนั้นใช้ได้
ปีเตอร์ - Reinstate Monica

6
@ user4581301 "มันดูเหมือนว่าจะทำงานเมื่อฉันพยายามมัน" เป็นสิ่งที่เลวร้ายที่สุดแน่นอนเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด
HerrJoebob

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

2
@HerrJoebob เห็นด้วยกับคำสั่ง 100% แต่ไม่ใช่บริบทที่คุณใช้มันวิธีที่ฉันอ่านคำถามนี้จะแก้ไขเป็น "เมื่ออายุการใช้งานของวัตถุสิ้นสุดลงแล้ว" การหาคำตอบเมื่อตัวเรียก destructor เป็นวิธีที่ดีในการทำเช่นนั้น สำหรับตัวแปรอัตโนมัติตัวทำลายควรถูกเรียกใช้ตรงเวลาหรือคุณมีปัญหาใหญ่
user4581301

คำตอบ:


18

ในรหัสของคุณทั้งสองs1และs3มีการอ้างอิงที่ห้อยอยู่ s2และs4ก็โอเค

ในการโทรครั้งแรกstd::stringวัตถุว่างชั่วคราวที่สร้างขึ้นจากอาร์กิวเมนต์เริ่มต้นจะถูกสร้างขึ้นในบริบทของการแสดงออกที่มีการโทร ดังนั้นมันจะตายในตอนท้ายของคำนิยามของs1ซึ่งใบs1ห้อย

ในการเรียกครั้งที่สองstd::stringวัตถุชั่วคราวจะถูกใช้เพื่อเริ่มต้นs2จากนั้นก็จะตาย

ในการเรียกครั้งที่สามสตริงตัวอักษร"s"จะถูกใช้เพื่อสร้างstd::stringวัตถุชั่วคราวและยังตายเมื่อสิ้นสุดคำจำกัดความของs3โดยปล่อยให้s3ห้อยอยู่

ในการเรียกครั้งที่สี่std::stringวัตถุชั่วคราวที่มีค่า"s"จะถูกใช้เพื่อเริ่มต้นs4แล้วจึงตาย

ดู C ++ 17 [class.temporary] /6.1

วัตถุชั่วคราวที่ถูกผูกไว้กับพารามิเตอร์อ้างอิงในการเรียกใช้ฟังก์ชัน (8.2.2) ยังคงมีอยู่จนกระทั่งการแสดงออกเต็มรูปแบบที่มีการเรียกเสร็จสมบูรณ์


1
ส่วนที่น่าสนใจของคำตอบคือการยืนยันว่าอาร์กิวเมนต์เริ่มต้นจะถูกสร้างขึ้นในบริบทของผู้โทร ที่ดูเหมือนว่าจะได้รับการสนับสนุนโดยอ้างมาตรฐานของ Guillaume
ปีเตอร์ - Reinstate Monica

2
@ Peter-ReinstateMonica ดู [expr.call] / 4, "... การเริ่มต้นและการทำลายของแต่ละพารามิเตอร์เกิดขึ้นภายในบริบทของฟังก์ชั่นการโทร ... "
Brian

8

มันเป็นความไม่ปลอดภัย :

โดยทั่วไปอายุการใช้งานของชั่วคราวไม่สามารถขยายได้อีกต่อไปโดย "ส่งต่อ": การอ้างอิงที่สองซึ่งเริ่มต้นจากการอ้างอิงที่การผูกชั่วคราวไม่ส่งผลต่ออายุการใช้งาน


ดังนั้นคุณคิดว่าstd::string s2 = foo();ถูกต้อง (หลังจากทั้งหมดไม่มีการอ้างอิงที่ชัดเจน)
ปีเตอร์ - Reinstate Monica

1
@ Peter-ReinstateMonica ว่ามีความปลอดภัยตามที่วัตถุใหม่จะถูกสร้างขึ้น คำตอบของฉันเป็นเพียงการขยายอายุการใช้งาน อีกสองคำตอบครอบคลุมทุกอย่างแล้ว ฉันจะไม่พูดซ้ำในของฉัน
ให้อภัย

5

ขึ้นอยู่กับสิ่งที่คุณทำกับสตริงหลังจากนั้น

หากคำถามของคุณคือรหัสของฉันถูกต้อง? ใช่แล้วใช่

จาก [dcl.fct.default] / 2

[ ตัวอย่าง : การประกาศ

void point(int = 3, int = 4);

ประกาศฟังก์ชั่นที่สามารถเรียกว่ามีศูนย์หนึ่งหรือสองข้อโต้แย้งของประเภท int สามารถเรียกได้หลายวิธี:

point(1,2);  point(1);  point();

การเรียกสองครั้งสุดท้ายเทียบเท่ากับpoint(1,4)และpoint(3,4)ตามลำดับ - ตัวอย่างท้าย ]

ดังนั้นโค้ดของคุณจึงเทียบเท่ากับ:

const std::string& s1 = foo(std::string(""));
std::string s2 = foo(std::string(""));

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

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

const std::string& s1 = foo(std::string("")); // okay

s1; // not okay, s1 is dead. s1 is the temporary.

ตัวอย่างของคุณs2ไม่เป็นไรตั้งแต่คุณคัดลอก (หรือย้าย) จากชั่วคราวก่อนสิ้นสุดการชำระเงิน มีปัญหาเดียวกันกว่าs3s1

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.