Đây có phải là một cạm bẫy đã biết của C ++ 11 cho các vòng lặp không?


89

Hãy tưởng tượng chúng ta có một cấu trúc để giữ 3 bộ đôi với một số hàm thành viên:

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

Điều này hơi giả tạo vì sự đơn giản, nhưng tôi chắc chắn rằng bạn đồng ý rằng mã tương tự hiện có. Các phương pháp cho phép bạn xâu chuỗi một cách thuận tiện, ví dụ:

Vector v = ...;
v.normalize().negate();

Hoặc thậm chí:

Vector v = Vector{1., 2., 3.}.normalize().negate();

Bây giờ nếu chúng tôi cung cấp các hàm begin () và end (), chúng tôi có thể sử dụng Vector của mình trong một vòng lặp for kiểu mới, chẳng hạn như lặp qua 3 tọa độ x, y và z (chắc chắn bạn có thể xây dựng các ví dụ "hữu ích" hơn bằng cách thay thế Vector bằng ví dụ: Chuỗi):

Vector v = ...;
for (double x : v) { ... }

Chúng tôi thậm chí có thể làm:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

và cả:

for (double x : Vector{1., 2., 3.}) { ... }

Tuy nhiên, điều sau (có vẻ như đối với tôi) đã bị hỏng:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

Mặc dù nó có vẻ như là một sự kết hợp hợp lý của hai cách sử dụng trước, tôi nghĩ cách sử dụng cuối cùng này tạo ra một tham chiếu lơ lửng trong khi hai cách sử dụng trước đó hoàn toàn ổn.

  • Điều này có chính xác và được đánh giá cao không?
  • Phần nào trên đây là phần "xấu", cần tránh?
  • Liệu ngôn ngữ có được cải thiện bằng cách thay đổi định nghĩa của vòng lặp for dựa trên phạm vi để các dấu tạm thời được xây dựng trong biểu thức for tồn tại trong suốt thời gian của vòng lặp không?

Vì lý do nào đó, tôi nhớ lại một câu hỏi tương tự đã được hỏi trước đây, nhưng tôi đã quên nó được gọi là gì.
Pubby

Tôi coi đây là một khiếm khuyết về ngôn ngữ. Tuổi thọ của vòng lặp tạm thời không được kéo dài cho toàn bộ phần thân của vòng lặp for, mà chỉ dành cho việc thiết lập vòng lặp for. Nó không chỉ là cú pháp phạm vi bị ảnh hưởng, cú pháp cổ điển cũng vậy. Theo ý kiến ​​của tôi, tuổi thọ của các vòng lặp tạm thời trong câu lệnh init nên kéo dài cho toàn bộ vòng đời của vòng lặp.
edA-qa mort-ora-y

1
@ edA-qamort-ora-y: Tôi có xu hướng đồng ý rằng có một chút khiếm khuyết về ngôn ngữ ẩn náu ở đây, nhưng tôi nghĩ rằng thực tế cụ thể là phần mở rộng thời gian diễn ra ngầm bất cứ khi nào bạn trực tiếp liên kết tạm thời với một tham chiếu, nhưng không phải trong bất kỳ tình huống khác - đây có vẻ như là một giải pháp nửa vời cho vấn đề cơ bản của cuộc sống tạm thời, mặc dù điều đó không có nghĩa là rõ ràng giải pháp tốt hơn sẽ là gì. Có lẽ cú pháp 'phần mở rộng trọn đời' rõ ràng khi xây dựng khối tạm thời, khiến nó kéo dài đến cuối khối hiện tại - bạn nghĩ sao?
ndkrempel

@ edA-qamort-ora-y: ... điều này tương tự như việc liên kết tạm thời với một tham chiếu, nhưng có lợi thế là rõ ràng hơn cho người đọc rằng 'mở rộng thời gian tồn tại' đang xảy ra, nội dòng (trong một biểu thức , thay vì yêu cầu một khai báo riêng biệt) và không yêu cầu bạn đặt tên tạm thời.
ndkrempel

Câu trả lời:


64

Điều này có chính xác và được đánh giá cao không?

Vâng, sự hiểu biết của bạn về mọi thứ là đúng.

Phần nào trên đây là phần "xấu", cần tránh?

Phần xấu là lấy một tham chiếu giá trị l đến một tham chiếu tạm thời được trả về từ một hàm và ràng buộc nó với một tham chiếu giá trị r. Nó cũng tệ như thế này:

auto &&t = Vector{1., 2., 3.}.normalize();

Thời Vector{1., 2., 3.}gian tồn tại của tạm thời không thể được kéo dài vì trình biên dịch không biết rằng giá trị trả về từ normalizetham chiếu nó.

Liệu ngôn ngữ có được cải thiện bằng cách thay đổi định nghĩa của vòng lặp for dựa trên phạm vi để các dấu tạm thời được xây dựng trong biểu thức for tồn tại trong suốt thời gian của vòng lặp không?

Điều đó sẽ rất mâu thuẫn với cách hoạt động của C ++.

Liệu nó có ngăn chặn một số lỗi nhất định được thực hiện bởi những người sử dụng các biểu thức chuỗi trên thời gian tạm thời hoặc các phương pháp đánh giá lười biếng khác nhau cho các biểu thức không? Đúng. Nhưng nó cũng sẽ yêu cầu mã trình biên dịch trường hợp đặc biệt, cũng như gây nhầm lẫn tại sao nó không hoạt động với các cấu trúc biểu thức khác .

Một giải pháp hợp lý hơn nhiều sẽ là một số cách để thông báo cho trình biên dịch rằng giá trị trả về của một hàm luôn là một tham chiếu đến this, và do đó nếu giá trị trả về được liên kết với một cấu trúc mở rộng tạm thời, thì nó sẽ mở rộng tạm thời đúng. Đó là một giải pháp cấp độ ngôn ngữ.

Hiện tại (nếu trình biên dịch hỗ trợ nó), bạn có thể làm cho nó normalize không thể được gọi tạm thời:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Điều này sẽ gây ra Vector{1., 2., 3.}.normalize()lỗi biên dịch, trong khi v.normalize()vẫn hoạt động tốt. Rõ ràng là bạn sẽ không thể làm những việc chính xác như thế này:

Vector t = Vector{1., 2., 3.}.normalize();

Nhưng bạn cũng sẽ không thể làm những điều không đúng.

Ngoài ra, như được đề xuất trong các nhận xét, bạn có thể đặt phiên bản tham chiếu rvalue trả về một giá trị thay vì một tham chiếu:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Nếu Vectorlà một loại có tài nguyên thực tế để di chuyển, bạn có thể sử dụng Vector ret = std::move(*this);thay thế. Tối ưu hóa giá trị trả lại được đặt tên làm cho điều này tối ưu một cách hợp lý về mặt hiệu suất.


1
Điều có thể làm cho điều này giống một "gotcha" hơn là vòng lặp for mới về mặt cú pháp che giấu thực tế rằng liên kết tham chiếu đang diễn ra dưới vỏ bọc - tức là nó ít trắng trợn hơn nhiều so với các ví dụ "chỉ là tồi tệ" của bạn ở trên. Đó là lý do tại sao có vẻ hợp lý khi đề xuất quy tắc mở rộng vòng đời bổ sung, chỉ dành cho vòng lặp for mới.
ndkrempel

1
@ndkrempel: Có, nhưng nếu bạn định đề xuất một tính năng ngôn ngữ để khắc phục điều này (và do đó ít nhất phải đợi đến năm 2017), tôi muốn nếu nó toàn diện hơn, một thứ có thể giải quyết vấn đề mở rộng tạm thời ở mọi nơi .
Nicol Bolas

3
+1. Ở cách tiếp cận cuối cùng, thay vì deletebạn có thể cung cấp một thao tác thay thế trả về giá trị: Vector normalize() && { normalize(); return std::move(*this); }(Tôi tin rằng lệnh gọi normalizebên trong hàm sẽ gửi đến quá tải giá trị, nhưng ai đó nên kiểm tra nó :)
David Rodríguez - dribeas

3
Tôi chưa bao giờ thấy điều này &/ trình &&độ của các phương pháp. Đây là từ C ++ 11 hay đây là một số tiện ích mở rộng trình biên dịch độc quyền (có thể phổ biến). Cung cấp khả năng bắt giữ.
Christian Rau

1
@ChristianRau: Nó mới đối với C ++ 11, và tương tự với C ++ 03 trình độ "const" và "dễ bay hơi" của các hàm thành viên không tĩnh, theo một nghĩa nào đó, nó đủ tiêu chuẩn "this". g ++ 4.7.0 tuy nhiên không hỗ trợ nó.
ndkrempel

25

for (double x: Vector {1., 2., 3.}. normalize ()) {...}

Đó không phải là hạn chế của ngôn ngữ mà là vấn đề với mã của bạn. Biểu thức Vector{1., 2., 3.}tạo ra một tham chiếu tạm thời, nhưng normalizehàm trả về một tham chiếu giá trị . Bởi vì biểu thức là một giá trị , trình biên dịch giả định rằng đối tượng sẽ còn sống, nhưng vì nó là một tham chiếu đến một tạm thời, đối tượng sẽ chết sau khi biểu thức đầy đủ được đánh giá, vì vậy bạn chỉ còn lại một tham chiếu treo lơ lửng.

Bây giờ, nếu bạn thay đổi thiết kế của mình để trả về một đối tượng mới theo giá trị thay vì một tham chiếu đến đối tượng hiện tại, thì sẽ không có vấn đề gì và mã sẽ hoạt động như mong đợi.


1
Một sẽ consttham khảo kéo dài tuổi thọ của đối tượng trong trường hợp này?
David Stone

5
Điều này sẽ phá vỡ ngữ nghĩa mong muốn rõ ràng của normalize()như một hàm đột biến trên một đối tượng hiện có. Vì vậy, câu hỏi. Đó là một tạm thời có "tuổi thọ kéo dài" khi được sử dụng cho mục đích cụ thể của một lần lặp lại, và không phải trường hợp khác, tôi nghĩ là một cách hiểu sai khó hiểu.
Andy Ross

2
@AndyRoss: Tại sao? Bất kỳ ràng buộc tạm thời nào với tham chiếu giá trị r (hoặc const&) đều có thời gian tồn tại của nó được kéo dài.
Nicol Bolas

2
@ndkrempel: Tuy nhiên, không phải là một giới hạn của dãy dựa trên vòng lặp for, cùng một vấn đề sẽ đến nếu bạn liên kết với một tài liệu tham khảo: Vector & r = Vector{1.,2.,3.}.normalize();. Thiết kế của bạn có hạn chế đó và điều đó có nghĩa là bạn sẵn sàng trả lại theo giá trị (điều này có thể có ý nghĩa trong nhiều trường hợp và hơn thế nữa với tham chiếudi chuyển rvalue ), hoặc nếu không, bạn cần phải xử lý vấn đề ở vị trí call: tạo một biến thích hợp, sau đó sử dụng nó trong vòng lặp for. Cũng lưu ý rằng biểu thức Vector v = Vector{1., 2., 3.}.normalize().negate();tạo ra hai đối tượng ...
David Rodríguez - dribeas

1
@ DavidRodríguez-dribeas: vấn đề với ràng buộc với const-reference là cái này: T const& f(T const&);hoàn toàn ổn. T const& t = f(T());là hoàn toàn ổn. Và sau đó, trong một TU khác, bạn phát hiện ra điều đó T const& f(T const& t) { return t; }và bạn khóc ... Nếu operator+hoạt động dựa trên các giá trị, nó an toàn hơn ; thì trình biên dịch có thể tối ưu hóa bản sao ra (Muốn Tốc độ? Chuyển theo Giá trị), nhưng đó là một phần thưởng. Ràng buộc duy nhất của các khoảng thời gian tạm thời mà tôi cho phép là ràng buộc với các tham chiếu giá trị r, nhưng các hàm sau đó sẽ trả về giá trị để an toàn và dựa vào Copy Elision / Move Semantics.
Matthieu M.

4

IMHO, ví dụ thứ hai đã thiếu sót. Việc trả về các toán tử sửa đổi *thislà thuận tiện theo cách bạn đã đề cập: nó cho phép xâu chuỗi các từ sửa đổi. Nó có thể được sử dụng để chuyển giao kết quả của việc sửa đổi một cách đơn giản, nhưng làm điều này rất dễ xảy ra lỗi vì nó có thể dễ dàng bị bỏ qua. Nếu tôi thấy một cái gì đó như

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

Tôi sẽ không nghi ngờ rằng các chức năng sửa đổi vnhư một tác dụng phụ. Tất nhiên, họ có thể , nhưng sẽ rất khó hiểu. Vì vậy, nếu tôi viết một cái gì đó như thế này, tôi sẽ đảm bảo rằng nó vkhông đổi. Đối với ví dụ của bạn, tôi sẽ thêm các chức năng miễn phí

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

và sau đó viết các vòng lặp

for( double x : negated(normalized(v)) ) { ... }

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

IMO đó dễ đọc hơn và an toàn hơn. Tất nhiên, nó yêu cầu một bản sao bổ sung, tuy nhiên đối với dữ liệu được phân bổ theo đống, điều này có thể được thực hiện trong một hoạt động di chuyển C ++ 11 rẻ tiền.


Cảm ơn. Như thường lệ, có rất nhiều sự lựa chọn. Một tình huống mà đề xuất của bạn có thể không khả thi là nếu Vector là một mảng (không được phân bổ theo heap) gồm 1000 nhân đôi, chẳng hạn. Đánh đổi hiệu quả, dễ sử dụng và an toàn khi sử dụng.
ndkrempel

2
Có, nhưng ít khi hữu ích khi có cấu trúc với kích thước> ≈100 trên ngăn xếp.
bùng binh
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.