Sự khác biệt về hành vi của hàm lambda có thể bắt được từ một tham chiếu đến biến toàn cục


22

Tôi thấy kết quả khác nhau giữa các trình biên dịch nếu tôi sử dụng lambda để thu thập tham chiếu đến biến toàn cục với từ khóa có thể thay đổi và sau đó sửa đổi giá trị trong hàm lambda.

#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;
}

Kết quả từ VS 2015 và GCC (g ++ (Ubuntu 5.4.0-6ubfox1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Kết quả từ clang ++ (phiên bản clang 3.8.0-2ubfox4 (thẻ / RELEASE_380 / trận chung kết)):

100 223 223

Lý do tại sao điều này xảy ra? Điều này có được phép theo Tiêu chuẩn C ++ không?


Hành vi của Clang vẫn còn trên thân cây.
quả óc chó

Đây là tất cả các phiên bản trình biên dịch khá cũ
MM

Nó vẫn trình bày trên phiên bản gần đây của Clang: godbolt.org/z/P9na9c
Willy

1
Nếu bạn loại bỏ hoàn toàn việc chụp, thì GCC vẫn chấp nhận mã này và thực hiện những gì clang làm. Đó là một gợi ý mạnh mẽ rằng có một lỗi GCC - chụp đơn giản không có nghĩa vụ phải thay đổi ý nghĩa của cơ thể lambda.
TC

Câu trả lời:


16

Một lambda không thể nắm bắt được một tài liệu tham khảo chính theo giá trị (sử dụng std::reference_wrappercho mục đích đó).

Trong lambda của bạn, các [m]ảnh chụp mtheo giá trị (vì không có &trong bản chụp), do đó m(là một tham chiếu đến n) trước tiên được bỏ qua và một bản sao của điều mà nó tham chiếu ( n) được chụp. Điều này không khác gì làm điều này:

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

Lambda sau đó sửa đổi bản sao đó, không phải bản gốc. Đó là những gì bạn đang thấy xảy ra trong các đầu ra của VS và GCC, như mong đợi.

Đầu ra Clang là sai và nên được báo cáo là một lỗi, nếu nó chưa xảy ra.

Nếu bạn muốn lambda của bạn sửa đổi n, mthay vào đó hãy chụp bằng cách tham khảo : [&m]. Điều này không khác gì việc gán một tham chiếu cho một tham chiếu khác, ví dụ:

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

Hoặc, bạn chỉ có thể thoát khỏi mhoàn toàn và chụp nbằng cách tham chiếu thay thế : [&n].

Mặc dù, ntrong phạm vi toàn cầu, nó thực sự không cần phải bị bắt, lambda có thể truy cập nó trên toàn cầu mà không cần nắm bắt:

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

5

Tôi nghĩ Clang thực sự có thể đúng.

Theo [lambda.capture] / 11 , một biểu thức id được sử dụng trong lambda chỉ đề cập đến thành viên bị bắt bởi bản sao của lambda chỉ khi nó cấu thành sử dụng odr . Nếu nó không, thì nó đề cập đến thực thể ban đầu . Điều này áp dụng cho tất cả các phiên bản C ++ kể từ C ++ 11.

Theo [basic.dev.odr] / 3 của C ++ 17, một biến tham chiếu không được sử dụng nếu sử dụng chuyển đổi lvalue sang rvalue cho nó mang lại một biểu thức không đổi.

Tuy nhiên, trong dự thảo C ++ 20, yêu cầu đối với chuyển đổi từ giá trị sang giá trị được loại bỏ và đoạn văn có liên quan đã thay đổi nhiều lần để bao gồm hoặc không bao gồm chuyển đổi. Xem vấn đề CWG 1472CWG phát hành 1741 , cũng như mở vấn đề CWG 2083 .

Do mđược khởi tạo với một biểu thức không đổi (tham chiếu đến một đối tượng thời lượng lưu trữ tĩnh), sử dụng nó mang lại một biểu thức không đổi cho mỗi ngoại lệ trong [expr.const] /2.11.1 .

Tuy nhiên, đây không phải là trường hợp nếu các chuyển đổi từ giá trị sang giá trị được áp dụng, bởi vì giá trị của nkhông thể sử dụng được trong một biểu thức không đổi.

Do đó, tùy thuộc vào việc chuyển đổi lvalue sang rvalue có được áp dụng trong việc xác định sử dụng odr hay không, khi bạn sử dụng mtrong lambda, nó có thể hoặc không thể đề cập đến thành viên của lambda.

Nếu chuyển đổi nên được áp dụng, GCC và MSVC là chính xác, nếu không thì Clang là.

Bạn có thể thấy Clang thay đổi hành vi nếu bạn thay đổi khởi tạo mthành không còn là biểu thức không đổi nữa:

#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;
}

Trong trường hợp này, tất cả các trình biên dịch đồng ý rằng đầu ra là

100 223 100

bởi vì mtrong lambda sẽ đề cập đến thành viên của bao đóng có kiểu intsao chép được khởi tạo từ biến tham chiếu mtrong f.


Cả hai kết quả VS / GCC & Clang có đúng không? Hay chỉ một trong số họ?
Willy

[basic.dev.odr] / 3 nói rằng biến mnày được sử dụng bởi một biểu thức đặt tên cho nó trừ khi áp dụng chuyển đổi lvalue-to-rvalue cho nó sẽ là một biểu thức không đổi. Theo [expr.const] / (2.7), chuyển đổi đó sẽ không phải là biểu thức hằng số cốt lõi.
aschepler

Nếu kết quả của Clang là chính xác, tôi nghĩ nó bằng cách nào đó phản trực giác. Bởi vì theo quan điểm của lập trình viên, anh ta cần chắc chắn rằng biến anh ta viết trong danh sách chụp thực sự được sao chép cho trường hợp có thể thay đổi và việc khởi tạo m có thể được lập trình viên thay đổi sau đó vì một số lý do.
Willy

1
m += 123;Đây mlà odr được sử dụng.
Oliv

1
Tôi nghĩ Clang đúng với cách diễn đạt hiện tại và mặc dù tôi chưa đi sâu vào vấn đề này, nhưng những thay đổi liên quan ở đây gần như chắc chắn là tất cả các DR.
TC

4

Điều này không được cho phép bởi Tiêu chuẩn C ++ 17, nhưng bởi một số dự thảo Tiêu chuẩn khác thì có thể như vậy. Nó phức tạp, vì những lý do không được giải thích trong câu trả lời này.

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

Đối với mỗi thực thể được bắt bởi bản sao, một thành viên dữ liệu không tĩnh không tên được khai báo trong kiểu đóng. Trình tự khai báo của các thành viên này là không xác định. Kiểu của một thành viên dữ liệu như vậy là kiểu được tham chiếu nếu thực thể là tham chiếu đến một đối tượng, tham chiếu giá trị cho loại hàm được tham chiếu nếu thực thể là tham chiếu đến hàm hoặc loại thực thể bị bắt tương ứng.

[m]nghĩa là biến mtrong fđược bắt bởi bản sao. Thực thể mlà một tham chiếu đến đối tượng, vì vậy kiểu đóng có một thành viên có kiểu là kiểu được tham chiếu. Đó là, loại thành viên là int, và không int&.

Vì tên mbên trong thân lambda đặt tên thành viên của đối tượng đóng và không phải là biến trong f(và đây là phần nghi vấn), nên câu lệnh m += 123;sửa đổi thành viên đó, là một intđối tượng khác ::n.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.