Tạo mã Lambda C ++ với các ký tự ban đầu trong C ++ 14


9

Tôi đang cố gắng để hiểu / làm rõ mã mã được tạo khi các ảnh chụp được chuyển đến lambdas, đặc biệt là trong các ảnh chụp init tổng quát được thêm vào trong C ++ 14.

Đưa ra các mẫu mã sau đây được liệt kê dưới đây là hiểu biết hiện tại của tôi về những gì trình biên dịch sẽ tạo ra.

Trường hợp 1: chụp theo giá trị / chụp mặc định theo giá trị

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

Sẽ tương đương với:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Vì vậy, có nhiều bản sao, một bản sao vào tham số hàm tạo và một bản sao vào thành viên, sẽ tốn kém cho các loại như vectơ, v.v.

Trường hợp 2: chụp theo tham chiếu / chụp mặc định bằng tham chiếu

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

Sẽ tương đương với:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

Tham số là tham chiếu và thành viên là tham chiếu nên không có bản sao. Đẹp cho các loại như vector, vv

Trường hợp 3:

Tổng quát bắt init

auto lambda = [x = 33]() { std::cout << x << std::endl; };

Theo tôi, đây là trường hợp tương tự như Trường hợp 1 theo nghĩa là nó được sao chép vào thành viên.

Tôi đoán là trình biên dịch tạo mã tương tự ...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Ngoài ra nếu tôi có những điều sau đây:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

Các nhà xây dựng sẽ trông như thế nào? Nó cũng di chuyển nó vào thành viên?


1
@ rafix07 Trong trường hợp đó, mã thông tin chi tiết được tạo thậm chí sẽ không biên dịch (nó cố gắng sao chép-khởi tạo thành viên ptr duy nhất từ ​​đối số). cppinsights rất hữu ích để có được ý chính chung, nhưng rõ ràng không thể trả lời câu hỏi này ở đây.
Max Langhof

Bạn dường như cho rằng có một bản dịch của lambda cho functor là bước đầu tiên của quá trình biên dịch, hoặc bạn chỉ đang tìm kiếm mã tương đương (ví dụ: hành vi tương tự)? Cách một trình biên dịch cụ thể tạo mã (và mã nào nó tạo ra) sẽ phụ thuộc vào trình biên dịch, phiên bản, kiến ​​trúc, cờ, v.v ... Vậy, bạn có yêu cầu một nền tảng cụ thể không? Nếu không, câu hỏi của bạn không thực sự có thể trả lời. Khác với mã được tạo thực tế có thể sẽ hiệu quả hơn các hàm xử lý mà bạn liệt kê (ví dụ: các hàm tạo nội tuyến, tránh các bản sao không cần thiết, v.v.).
Sander De Dycker

2
Nếu bạn quan tâm đến tiêu chuẩn C ++ nói gì về nó, hãy tham khảo [expr.prim.lambda] . Đó là quá nhiều để tóm tắt ở đây như là một câu trả lời.
Sander De Dycker

Câu trả lời:


2

Câu hỏi này không thể được trả lời đầy đủ trong mã. Bạn có thể viết mã "tương đương", nhưng tiêu chuẩn không được chỉ định theo cách đó.

Với cách đó, hãy đi sâu vào [expr.prim.lambda]. Điều đầu tiên cần lưu ý là các hàm tạo chỉ được đề cập trong [expr.prim.lambda.closure]/13:

Kiểu đóng được liên kết với biểu thức lambda không có hàm tạo mặc định nếu biểu thức lambda có hàm lambda và hàm tạo mặc định mặc định khác. Nó có một hàm tạo sao chép mặc định và hàm tạo di chuyển mặc định ([class.copy.ctor]). Nó có một toán tử gán bản sao xóa nếu lambda thể hiện có một lambda-chụp và sao chép defaulted và toán tử gán di chuyển khác ([class.copy.assign]). [ Lưu ý: Các hàm thành viên đặc biệt này được định nghĩa ngầm như bình thường và do đó có thể được xác định là đã xóa. - lưu ý cuối ]

Vì vậy, ngay lập tức con dơi, cần phải rõ ràng rằng các nhà xây dựng không chính thức làm thế nào để bắt các đối tượng được xác định. Bạn có thể nhận được khá gần (xem câu trả lời cppinsights.io), nhưng các chi tiết khác nhau (lưu ý cách mã trong câu trả lời đó cho trường hợp 4 không biên dịch).


Đây là những mệnh đề tiêu chuẩn chính cần thiết để thảo luận về trường hợp 1:

[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 được đặt tên được khai báo theo 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. Loại thành viên dữ liệu đó là loại đượ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ột thành viên của một liên minh ẩn danh sẽ không bị bắt bởi bản sao.

[expr.prim.lambda.capture]/11

Mọi biểu thức id trong câu lệnh ghép của biểu thức lambda là sử dụng odr của một thực thể được bắt bởi bản sao được chuyển thành quyền truy cập vào thành viên dữ liệu chưa được đặt tên tương ứng của kiểu đóng. [...]

[expr.prim.lambda.capture]/15

Khi biểu thức lambda được ước tính, các thực thể được bắt giữ bằng bản sao được sử dụng để khởi tạo trực tiếp từng thành viên dữ liệu không tĩnh tương ứng của đối tượng đóng kết quả và các thành viên dữ liệu không tĩnh tương ứng với các bắt giữ init được khởi tạo như được chỉ định bởi trình khởi tạo tương ứng (có thể là sao chép hoặc khởi tạo trực tiếp). [...]

Hãy áp dụng điều này cho trường hợp của bạn 1:

Trường hợp 1: chụp theo giá trị / chụp mặc định theo giá trị

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

Kiểu đóng của lambda này sẽ có một thành viên dữ liệu không tĩnh không tên (gọi nó là __x) loại int(vì xkhông phải là tham chiếu cũng không phải là hàm) và truy cập vào xbên trong cơ thể lambda được chuyển đổi thành quyền truy cập __x. Khi chúng ta đánh giá biểu thức lambda (tức là khi gán cho lambda), chúng ta khởi tạo trực tiếp __x với x.

Nói tóm lại, chỉ có một bản sao diễn ra . Hàm tạo của kiểu đóng không liên quan và không thể biểu thị điều này trong C ++ "bình thường" (lưu ý rằng kiểu đóng cũng không phải là kiểu tổng hợp ).


Chụp tham chiếu liên quan đến [expr.prim.lambda.capture]/12:

Một thực thể được chụp bằng tham chiếu nếu nó được chụp ngầm hoặc rõ ràng nhưng không bị bắt bởi bản sao. Không xác định liệu các thành viên dữ liệu không tĩnh không được đặt tên bổ sung có được khai báo theo kiểu đóng cho các thực thể được bắt bởi tham chiếu hay không. [...]

Có một đoạn khác về chụp tham chiếu các tài liệu tham khảo nhưng chúng tôi không làm điều đó ở bất cứ đâu.

Vì vậy, đối với trường hợp 2:

Trường hợp 2: chụp theo tham chiếu / chụp mặc định bằng tham chiếu

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

Chúng tôi không biết liệu một thành viên được thêm vào loại đóng cửa. xtrong cơ thể lambda có thể chỉ trực tiếp đề cập đến xbên ngoài. Điều này tùy thuộc vào trình biên dịch, và nó sẽ thực hiện điều này trong một số dạng ngôn ngữ trung gian (khác với trình biên dịch sang trình biên dịch), không phải là một chuyển đổi nguồn của mã C ++.


Ban đầu chụp được chi tiết trong [expr.prim.lambda.capture]/6:

Một init-Capture hoạt động như thể nó tuyên bố và nắm bắt rõ ràng một biến auto init-capture ;có dạng có vùng khai báo là câu lệnh ghép của biểu thức lambda, ngoại trừ:

  • (6.1) nếu việc chụp là bằng bản sao (xem bên dưới), thành viên dữ liệu không tĩnh được khai báo cho việc chụp và biến được coi là hai cách khác nhau để tham chiếu đến cùng một đối tượng, có thời gian tồn tại của dữ liệu không tĩnh thành viên, và không có bản sao và phá hủy bổ sung được thực hiện, và
  • (6.2) nếu việc chụp là do tham chiếu, thời gian tồn tại của biến sẽ kết thúc khi thời gian tồn tại của đối tượng đóng kết thúc.

Cho rằng, hãy xem trường hợp 3:

Trường hợp 3: Chụp init tổng quát

auto lambda = [x = 33]() { std::cout << x << std::endl; };

Như đã nêu, hãy tưởng tượng đây là một biến được tạo bởi auto x = 33;và được ghi lại rõ ràng bằng bản sao. Biến này chỉ "hiển thị" trong cơ thể lambda. Như đã lưu ý [expr.prim.lambda.capture]/15trước đó, việc khởi tạo thành viên tương ứng của kiểu đóng ( __xđối với hậu thế) là do trình khởi tạo đã cho khi đánh giá biểu thức lambda.

Để tránh nghi ngờ: Điều này không có nghĩa là mọi thứ được khởi tạo hai lần ở đây. Đây auto x = 33;là một "như thể" để kế thừa ngữ nghĩa của các ảnh chụp đơn giản và khởi tạo được mô tả là một sửa đổi cho các ngữ nghĩa đó. Chỉ có một khởi tạo xảy ra.

Điều này cũng bao gồm trường hợp 4:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

Thành viên kiểu đóng được khởi tạo bởi __p = std::move(unique_ptr_var)khi biểu thức lambda được ước tính (nghĩa là khi lđược gán cho). Truy cập vào ptrong cơ thể lambda được chuyển thành truy cập vào __p.


TL; DR: Chỉ có số lượng bản sao / khởi tạo / di chuyển tối thiểu được thực hiện (như người ta hy vọng / mong đợi). Tôi sẽ giả định rằng lambdas không được chỉ định theo cách chuyển đổi nguồn (không giống như đường cú pháp khác) chính xác bởi vì việc diễn đạt mọi thứ theo các nhà xây dựng sẽ đòi hỏi các hoạt động không cần thiết.

Tôi hy vọng điều này giải quyết những nỗi sợ hãi thể hiện trong câu hỏi :)


9

Trường hợp 1 [x](){} : Hàm tạo được tạo sẽ chấp nhận đối số của nó bằng cách consttham chiếu có thể đủ tiêu chuẩn để tránh các bản sao không cần thiết:

__some_compiler_generated_name(const int& x) : x_{x}{}

Trường hợp 2 [x&](){} : Giả định của bạn ở đây là chính xác, xđược thông qua và lưu trữ bằng cách tham chiếu.


Trường hợp 3 [x = 33](){} : Một lần nữa đúng, xđược khởi tạo bởi giá trị.


Trường hợp 4 [p = std::move(unique_ptr_var)] : Hàm tạo sẽ như thế này:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

vì vậy, có, unique_ptr_var"được chuyển vào" đóng cửa. Xem thêm Mục 32 của Scott Meyer trong C ++ hiện đại hiệu quả ("Sử dụng init init để di chuyển các đối tượng vào trạng thái đóng").


" constĐủ tiêu chuẩn" Tại sao?
cpplearner

@cpplearner Mh, câu hỏi hay. Tôi đoán tôi đã chèn vào đó bởi vì một trong những chủ nghĩa tự động tinh thần đã khởi động ^^ Ít nhất là constkhông thể bị tổn thương ở đây do một sự mơ hồ / kết hợp tốt hơn khi không const... v.v ... Dù sao, bạn có nghĩ rằng tôi nên loại bỏ const?
Lubgr

Tôi nghĩ const nên vẫn còn, cái gì, nếu đối số được truyền cho thực sự là const?
Aconcagua

Vì vậy, bạn đang nói rằng hai công trình di chuyển (hoặc sao chép) xảy ra ở đây?
Max Langhof

Xin lỗi, ý tôi là trong trường hợp 4 (đối với di chuyển) và trường hợp 1 (đối với các bản sao). Phần sao chép câu hỏi của tôi không có ý nghĩa dựa trên các câu của bạn (nhưng tôi nghi ngờ những câu đó).
Max Langhof

5

Có ít nhu cầu suy đoán, sử dụng cppinsights.io .

Trường hợp 1:

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

Trình biên dịch tạo

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Trường hợp 2:

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

Trình biên dịch tạo

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Trường hợp 3:

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

Trình biên dịch tạo

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

Trường hợp 4 (không chính thức):

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

Trình biên dịch tạo

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

Và tôi tin rằng đoạn mã cuối cùng này trả lời câu hỏi của bạn. Di chuyển xảy ra, nhưng không [về mặt kỹ thuật] trong hàm tạo.

Bản thân chụp không có const, nhưng bạn có thể thấy rằng operator()chức năng là. Đương nhiên, nếu bạn cần sửa đổi các ảnh chụp, bạn đánh dấu lambda là mutable.


Mã bạn hiển thị cho trường hợp cuối cùng thậm chí không biên dịch. Kết luận "một động thái xảy ra, nhưng không [về mặt kỹ thuật] trong hàm tạo" không thể được hỗ trợ bởi mã đó.
Max Langhof

Các luật của trường hợp 4 chắc chắn nhất không biên dịch trên máy Mac của tôi. Tôi ngạc nhiên rằng mã được mở rộng được tạo từ cppinsights không biên dịch. Các trang web, đến thời điểm này, là khá đáng tin cậy đối với tôi. Tôi sẽ nêu vấn đề với họ. EDIT: Tôi đã xác nhận rằng mã được tạo không biên dịch; Điều đó không rõ ràng nếu không có chỉnh sửa này.
lộng lẫy

1
Liên kết đến vấn đề trong trường hợp quan tâm: github.com/andreasfertig/cppinsights/issues/258 Tôi vẫn đề xuất trang web cho những việc như kiểm tra SFINAE và liệu có hay không diễn xuất ẩn.
lộng lẫy
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.