ความแตกต่างของพฤติกรรมของฟังก์ชั่นแลมบ์ดาที่จับได้ไม่แน่นอนจากการอ้างอิงถึงตัวแปรทั่วโลก


22

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

#include <stdio.h>
#include <functional>

int n = 100;

std::function<int()> f()
{
    int &m = n;
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

ผลลัพธ์จาก VS 2015 และ GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

ผลลัพธ์จากเสียงดังกังวาน ++ (เสียงดังกราวรุ่น 3.8.0-2ubuntu4 (แท็ก / RELEASE_380 / สุดท้าย)):

100 223 223

ทำไมสิ่งนี้ถึงเกิดขึ้น สิ่งนี้ได้รับอนุญาตจากมาตรฐาน C ++ หรือไม่


พฤติกรรมของเสียงดังกราวยังคงอยู่บนลำต้น
วอลนัท

นี่เป็นคอมไพเลอร์เวอร์ชั่นเก่าทั้งหมด
MM

มันยังคงแสดงอยู่ใน Clang รุ่นล่าสุด: godbolt.org/z/P9na9c
Willy

1
หากคุณนำการจับออกทั้งหมด GCC จะยังคงยอมรับรหัสนี้และทำสิ่งที่มีเสียงดังกราว นั่นเป็นคำใบ้ที่แข็งแกร่งว่ามีบั๊ก GCC - การจับภาพอย่างง่ายไม่ควรเปลี่ยนความหมายของร่างกายแลมบ์ดา
TC

คำตอบ:


16

แลมบ์ดาไม่สามารถรวบรวมการอ้างอิงได้ด้วยตัวเองตามมูลค่า (ใช้std::reference_wrapperเพื่อจุดประสงค์นั้น)

ในแลมบ์ดาของคุณการ[m]จับภาพmตามค่า (เนื่องจากไม่มี&ในการดักจับ) ดังนั้นm(อ้างอิงถึงn) จึงถูกยกเลิกการลงทะเบียนครั้งแรกและคัดลอกสิ่งที่อ้างอิง ( n) สิ่งนี้ไม่ต่างไปจากการทำสิ่งนี้:

int &m = n;
int x = m; // <-- copy made!

แลมบ์ดาจะแก้ไขสำเนานั้นไม่ใช่ต้นฉบับ นั่นคือสิ่งที่คุณเห็นเกิดขึ้นในเอาต์พุต VS และ GCC ตามที่คาดไว้

เอาต์พุตเสียงดังกราวผิดและควรรายงานว่าเป็นข้อผิดพลาดหากยังไม่ได้ส่ง

หากคุณต้องการแลมบ์ดาของคุณในการปรับเปลี่ยนnการจับภาพโดยการอ้างอิงแทน:m [&m]สิ่งนี้ไม่แตกต่างจากการมอบหมายการอ้างอิงหนึ่งไปยังอีกการอ้างอิงเช่น:

int &m = n;
int &x = m; // <-- no copy made!

หรือคุณก็สามารถกำจัดmโดยสิ้นเชิงและการจับภาพโดยการอ้างอิงแทน:n[&n]

แม้ว่าnแลมบ์ดาจะสามารถเข้าถึงได้ทั่วโลกแม้ว่าจะอยู่ในขอบเขตทั่วโลก แต่ก็ไม่จำเป็นต้องถูกดักจับเลย

return [] () -> int {
    n += 123;
    return n;
};

5

ฉันคิดว่าเสียงดังกังวานอาจจะถูกต้องจริง

ตาม[lambda.capture] / 11เป็นรหัสการแสดงออกที่ใช้ในแลมบ์ดาหมายถึงแลมบ์ดาโดยการคัดลอกจับสมาชิกเท่านั้นถ้ามันถือว่าเป็นODR การใช้งาน ถ้ามันไม่ได้แล้วมันหมายถึงนิติบุคคลเดิม สิ่งนี้ใช้ได้กับ C ++ ทุกรุ่นตั้งแต่ C ++ 11

ตามการอ้างอิงพื้นฐานของ C ++ 17 [Basic.dev.odr] / 3ไม่ได้ถูกใช้ถ้าใช้การแปลง lvalue-to-rvalue กับมันทำให้เกิดการแสดงออกอย่างต่อเนื่อง

ในแบบร่าง C ++ 20 อย่างไรก็ตามข้อกำหนดสำหรับการแปลง lvalue-to-rvalue ถูกทิ้งและข้อความที่เกี่ยวข้องเปลี่ยนไปหลายครั้งเพื่อรวมหรือไม่รวมการแปลง ดูCWG ปัญหา 1472และCWG ปัญหา 1741เช่นเดียวกับการเปิดประเด็น CWG 2083

ตั้งแต่mเริ่มต้นด้วยการแสดงออกอย่างต่อเนื่อง (หมายถึงแบบคงที่ระยะเวลาการเก็บรักษาวัตถุ) ที่ใช้มันมีอัตราผลตอบแทนการแสดงออกอย่างต่อเนื่องต่อข้อยกเว้นใน[expr.const] /2.11.1

นี่ไม่ใช่กรณีอย่างไรก็ตามหากมีการใช้การแปลงแบบ lvalue-to-rvalue เนื่องจากค่าของnไม่สามารถใช้งานได้ในนิพจน์คงที่

ดังนั้นขึ้นอยู่กับว่าการแปลงแบบ lvalue-to-rvalue นั้นควรถูกนำไปใช้ในการพิจารณา odr-use หรือไม่เมื่อคุณใช้mใน lambda มันอาจจะใช่หรือไม่ใช่สมาชิกของแลมบ์ดา

หากการแปลงควรนำไปใช้ GCC และ MSVC ถูกต้องมิฉะนั้นเสียงดังกราวเป็น

คุณจะเห็นว่าเสียงดังกราวเปลี่ยนพฤติกรรมถ้าคุณเปลี่ยนการเริ่มต้นmเป็นไม่คงที่อีกต่อไป:

#include <stdio.h>
#include <functional>

int n = 100;

void g() {}

std::function<int()> f()
{
    int &m = (g(), n);
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

ในกรณีนี้คอมไพเลอร์ทั้งหมดยอมรับว่าผลลัพธ์เป็น

100 223 100

เพราะmในแลมบ์ดาจะอ้างถึงสมาชิกปิดของซึ่งเป็นประเภทของintการคัดลอกเริ่มต้นจากตัวแปรอ้างอิงในmf


ผลลัพธ์ VS / GCC และ Clang ทั้งสองถูกต้องหรือไม่ หรือเพียงหนึ่งในนั้น?
Willy

[basic.dev.odr] / 3 กล่าวว่าตัวแปรmนั้นใช้ odr โดยนิพจน์ที่ตั้งชื่อมันเว้นแต่ว่าจะใช้การแปลง lvalue-to-rvalue กับมันจะเป็นการแสดงออกที่คงที่ โดย [expr.const] / (2.7) การแปลงนั้นจะไม่เป็นการแสดงออกคงที่หลัก
aschepler

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

1
m += 123;นี่mคือ odr-used
Oliv

1
ฉันคิดว่าเสียงดังกังวานถูกต้องตามถ้อยคำปัจจุบันและแม้ว่าฉันจะไม่ได้ขุดลงไปสิ่งนี้การเปลี่ยนแปลงที่เกี่ยวข้องที่นี่เกือบจะแน่นอน DRs ทั้งหมด
TC

4

สิ่งนี้ไม่ได้รับอนุญาตจากมาตรฐาน C ++ 17 แต่โดยร่างมาตรฐานอื่น ๆ มันซับซ้อนด้วยเหตุผลที่ไม่ได้อธิบายไว้ในคำตอบนี้

[expr.prim.lambda.capture] / 10 :

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

[m]หมายความว่าตัวแปรmในการfถูกจับโดยการคัดลอก เอนทิตีmเป็นการอ้างอิงถึงวัตถุดังนั้นประเภทการปิดมีสมาชิกที่มีประเภทเป็นประเภทอ้างอิง นั่นคือประเภทของสมาชิกเป็นและไม่intint&

ตั้งแต่ชื่อmภายในร่างกายชื่อแลมบ์ดาสมาชิกของวัตถุปิดและไม่ได้อยู่ในตัวแปรf(และนี่คือส่วนหนึ่งที่น่าสงสัย) คำสั่งm += 123;ปรับเปลี่ยนสมาชิกซึ่งเป็นที่แตกต่างกันของวัตถุจากint::n

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