Vòng lặp 'for' dựa trên phạm vi có phản đối nhiều thuật toán đơn giản không?


81

Giải pháp thuật toán:

std::generate(numbers.begin(), numbers.end(), rand);

Giải pháp vòng lặp dựa trên phạm vi:

for (int& x : numbers) x = rand();

Tại sao tôi muốn sử dụng nhiều chi tiết std::generatehơn qua các vòng for dựa trên phạm vi trong C ++ 11?


14
Khả năng kết hợp? Oh không bao giờ tâm trí, các thuật toán với vòng lặp thường không composable anyway ... :(
R. Martinho Fernandes

2
... cái nào không begin()end()?
the_mandrill

6
@jrok Tôi hy vọng nhiều người có một rangechức năng trong hộp công cụ của họ bây giờ. (tức là for(auto& x : range(first, last)))
R. Martinho Fernandes

14
boost::generate(numbers, rand); // ♪
Xeo

5
@JamesBrock Chúng tôi đã thảo luận về điều này thường xuyên trong phòng trò chuyện C ++ (nó phải ở đâu đó trong bảng điểm: P). Vấn đề chính là các thuật toán thường trả về một trình vòng lặp và lấy hai trình vòng lặp.
R. Martinho Fernandes

Câu trả lời:


79

Phiên bản đầu tiên

std::generate(numbers.begin(), numbers.end(), rand);

cho chúng tôi biết rằng bạn muốn tạo một chuỗi giá trị.

Trong phiên bản thứ hai, người đọc sẽ phải tự mình tìm ra điều đó.

Tiết kiệm khi đánh máy thường không tối ưu, vì nó thường bị mất thời gian đọc. Hầu hết các mã được đọc nhiều hơn là được gõ.


13
Tiết kiệm khi nhập? Ồ, tôi hiểu rồi. Tại sao oh tại sao chúng ta có cùng một thuật ngữ cho "kiểm tra độ tỉnh táo trong thời gian biên dịch" và "nhấn các phím trên bàn phím"? :)
fredoverflow

25
" Tiết kiệm khi đánh máy thường là không tối ưu " Vô nghĩa; đó là tất cả về thư viện bạn đang sử dụng. std :: create dài vì bạn phải chỉ định numbershai lần mà không có lý do. Do đó: boost::range::generate(numbers, rand);. Không có lý do gì bạn không thể có cả mã ngắn hơn và dễ đọc hơn trong một thư viện được xây dựng tốt.
Nicol Bolas

9
Đó là tất cả trong mắt của người đọc. Phiên bản vòng lặp for có thể hiểu được với hầu hết các nền tảng lập trình: đặt giá trị rand cho mỗi phần tử của bộ sưu tập. Std :: create yêu cầu phải biết C ++ gần đây hoặc đoán tạo thực sự có nghĩa là "sửa đổi các mục", không phải "trả về giá trị đã tạo".
hyde

2
Nếu bạn chỉ muốn sửa đổi một phần của vùng chứa thì bạn có thể std::generate(number.begin(), numbers.begin()+3, rand), phải không? Vì vậy, tôi đoán để chỉ định numberhai lần đôi khi có thể hữu ích.
Marson Mao

7
@MarsonMao: nếu bạn chỉ có hai đối số std::generate(), thay vào đó bạn có thể làm std::generate(slice(number.begin(), 3), rand)hoặc thậm chí tốt hơn với cú pháp cắt phạm vi giả định như cú pháp std::generate(number[0:3], rand)loại bỏ sự lặp lại numbertrong khi vẫn cho phép đặc tả linh hoạt của một phần phạm vi. Làm ngược lại bắt đầu từ một lập luận ba std::generate()thì tẻ nhạt hơn.
Lie Ryan

42

Cho dù vòng lặp for có dựa trên phạm vi hay không không tạo ra sự khác biệt nào cả, nó chỉ đơn giản hóa mã bên trong dấu ngoặc đơn. Các thuật toán rõ ràng hơn ở chỗ chúng thể hiện ý định .


30

Cá nhân tôi, đọc đầu tiên của tôi về:

std::generate(numbers.begin(), numbers.end(), rand);

là "chúng tôi đang gán cho mọi thứ trong một phạm vi. Phạm vi là numbers. Các giá trị được chỉ định là ngẫu nhiên".

Đọc đầu tiên của tôi về:

for (int& x : numbers) x = rand();

là "chúng tôi đang làm điều gì đó với mọi thứ trong một phạm vi. Phạm vi là numbers. Những gì chúng tôi làm là chỉ định một giá trị ngẫu nhiên."

Chúng khá giống nhau, nhưng không giống nhau. Một lý do hợp lý mà tôi có thể muốn kích động lần đọc đầu tiên, là bởi vì tôi nghĩ sự thật quan trọng nhất về mã này là nó gán cho phạm vi. Vì vậy, có "tại sao tôi muốn ..." của bạn. Tôi sử dụng generatevì trong C ++ std::generatecó nghĩa là "gán phạm vi". Như btw std::copy, sự khác biệt giữa hai là những gì bạn chỉ định từ.

Tuy nhiên, có những yếu tố gây nhiễu. Các vòng lặp for dựa trên phạm vi có cách thể hiện trực tiếp hơn về phạm vi đó numbers, so với các thuật toán dựa trên trình lặp. Đó là lý do tại sao mọi người làm việc trên các thư viện thuật toán dựa trên phạm vi: boost::range::generate(numbers, rand);trông đẹp hơn std::generatephiên bản.

Ngược lại, int&trong vòng lặp for dựa trên phạm vi của bạn là một nếp nhăn. Điều gì sẽ xảy ra nếu kiểu giá trị của phạm vi không phải là int, thì chúng ta đang làm một điều gì đó rất tinh vi ở đây phụ thuộc vào việc nó có thể chuyển đổi thành int&, trong khi generatemã chỉ phụ thuộc vào kết quả trả về từ randviệc có thể gán cho phần tử. Ngay cả khi loại giá trị là int, tôi vẫn có thể dừng lại để suy nghĩ xem nó có hay không. Do đó auto, điều này làm giảm suy nghĩ về các loại cho đến khi tôi thấy những gì được chỉ định - với auto &xtôi nói "tham chiếu đến phần tử phạm vi, bất kỳ loại nào có thể có". Trở lại trong C ++ 03, các thuật toán (vì họ chức năng đang templates) là những cách để ẩn các loại chính xác, bây giờ họ đang một chiều.

Tôi nghĩ rằng các thuật toán đơn giản nhất chỉ có lợi ích biên so với các vòng lặp tương đương. Các vòng lặp dựa trên phạm vi cải thiện các vòng lặp (chủ yếu bằng cách loại bỏ hầu hết các bản ghi sẵn, mặc dù có nhiều hơn một chút đối với chúng). Vì vậy, lợi nhuận thu hẹp hơn và có lẽ bạn thay đổi ý định trong một số trường hợp cụ thể. Nhưng vẫn có một sự khác biệt về phong cách ở đó.


Bạn đã bao giờ thấy một loại do người dùng xác định với một operator int&()? :)
fredoverflow

@FredOverflow thay thế int&bằng SomeClass&và bây giờ bạn phải lo lắng về các toán tử chuyển đổi và các hàm tạo tham số đơn không được đánh dấu explicit.
TemplateRex

@FredOverflow: đừng nghĩ vậy. Đó là lý do tại sao nếu điều đó xảy ra, tôi sẽ không mong đợi điều đó và cho dù tôi có hoang tưởng về điều đó như thế nào bây giờ, nó sẽ cắn tôi nếu tôi không tình cờ nghĩ ra sau đó ;-) Một đối tượng proxy có thể hoạt động bằng cách quá tải operator int&()operator int const &() const, nhưng sau đó nó có thể hoạt động bằng cách quá tải operator int() constoperator=(int).
Steve Jessop

1
@rhalbersma: Tôi không nghĩ bạn phải lo lắng về các hàm tạo, vì tham chiếu không phải const không liên kết với tạm thời. Đó chỉ là các toán tử chuyển đổi thành các loại tham chiếu.
Steve Jessop 11/113

23

Theo ý kiến ​​của tôi, STL hiệu quả Mục 43: "Thuật toán ưu tiên các cuộc gọi đến các vòng lặp viết tay." vẫn là một lời khuyên tốt.

Tôi thường viết các hàm wrapper để thoát khỏi begin()/ end()hell. Nếu bạn làm điều đó, ví dụ của bạn sẽ giống như sau:

my_util::generate(numbers, rand);

Tôi tin rằng nó đánh bại phạm vi dựa trên vòng lặp for cả trong việc truyền đạt ý định và khả năng đọc.


Phải nói rằng, tôi phải thừa nhận rằng trong C ++ 98, một số lệnh gọi thuật toán STL mang lại mã không thể thay đổi được và việc tuân theo "Các lệnh gọi thuật toán ưu tiên đến các vòng lặp viết tay" dường như không phải là một ý tưởng hay. May mắn thay, lambdas đã thay đổi điều đó.

Hãy xem xét ví dụ sau từ Herb Sutter: Lambdas, Lambdas Everywhere .

Nhiệm vụ: Tìm phần tử đầu tiên trong v là > x< y.

Không có lambdas:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

Với lambda

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );

1
Một chút trực giao với câu hỏi. Chỉ câu đầu tiên giải quyết câu hỏi.
David Rodríguez - dribeas

@ DavidRodríguez-dribeas Có. Phần thứ hai là giải thích lý do tại sao tôi nghĩ Mục 43 vẫn là một lời khuyên tốt.
Ali

Với Boost.Lambda, nó thậm chí còn tốt hơn với các hàm lambda trong C ++: auto i = find_if (v.begin (), v.end (), _1> x && _1 <y);
sdkljhdf hda

1
+1 cho trình bao bọc. Làm giống như vậy. Đáng lẽ phải ở trong tiêu chuẩn từ ngày 1 (hoặc có thể là 2 ...)
Macke

22

Theo ý kiến của tôi , vòng lặp thủ công, mặc dù có thể làm giảm độ dài, nhưng thiếu tính dễ đọc:

for (int& x : numbers) x = rand();

Tôi sẽ không sử dụng vòng lặp này để khởi tạo 1 phạm vi được xác định bởi các số , bởi vì khi tôi nhìn vào nó, đối với tôi, có vẻ như nó đang lặp lại trên một phạm vi số, nhưng thực tế thì không (về bản chất), tức là thay vì đọc từ phạm vi, nó đang ghi vào phạm vi.

Ý định rõ ràng hơn nhiều khi bạn sử dụng std::generate.

1. khởi tạo trong ngữ cảnh này có nghĩa là cung cấp giá trị có ý nghĩa cho các phần tử của vùng chứa.


5
Tuy nhiên, đó không phải chỉ vì bạn không quen với các vòng lặp for dựa trên phạm vi sao? Đối với tôi, có vẻ khá rõ ràng rằng câu lệnh này chỉ định cho từng phần tử trong phạm vi. Rõ ràng là create thực hiện cùng một thứ mà bạn quen thuộc std::generate, điều này có thể được cho là của một lập trình viên C ++ (nếu họ không quen, họ sẽ tra cứu nó, cùng một kết quả).
Steve Jessop

4
@SteveJessop: Câu trả lời này không khác với hai câu kia. Nó đòi hỏi người đọc nỗ lực nhiều hơn một chút và dễ bị lỗi hơn một chút (nếu bạn quên một &ký tự thì sao?) Ưu điểm của các thuật toán là chúng thể hiện ý định, trong khi với các vòng lặp, bạn phải suy ra điều đó. Nếu có một lỗi trong quá trình thực hiện vòng lặp, không rõ đó là lỗi hay cố ý.
David Rodríguez - dribeas

1
@ DavidRodríguez-dribeas: câu trả lời này khác với hai câu kia, IMO đáng kể. Nó cố gắng đi sâu vào lý do mà tác giả thấy một đoạn mã rõ ràng / dễ hiểu hơn đoạn mã kia. Những người khác nêu điều đó mà không cần phân tích. Đó là lý do tại sao tôi tìm thấy điều này một thú vị, đủ để đáp ứng với nó :-)
Steve Jessop

1
@SteveJessop: Bạn phải xem xét phần thân của vòng lặp để đi đến kết luận rằng bạn thực sự đang tạo ra các con số, nhưng trong trường hợp std::generate, chỉ bằng một cái nhìn đơn thuần, người ta có thể nói rằng một cái gì đó đang được tạo ra bởi hàm này; cái gì đó được trả lời bởi đối số thứ ba cho hàm. Tôi nghĩ điều này tốt hơn nhiều.
Nawaz

1
@SteveJessop: Vậy có nghĩa là bạn thuộc nhóm thiểu số. Tôi sẽ viết mã rõ ràng hơn cho đa số: P. Điều cuối cùng: Tôi không nói ở đâu rằng những người khác sẽ đọc vòng lặp theo cách giống như tôi đã làm. Tôi đã nói (đúng hơn là có ý ) rằng đây là một cách để đọc vòng lặp gây hiểu lầm cho tôi, và bởi vì phần thân của vòng lặp ở đó, các lập trình viên khác nhau sẽ đọc nó theo cách khác nhau để tìm ra điều gì đang xảy ra ở đó; họ có thể phản đối việc sử dụng vòng lặp như vậy, vì những lý do khác nhau, tất cả đều có thể đúng theo nhận thức của họ.
Nawaz

9

Có một số điều bạn không thể làm (đơn giản) với các vòng lặp dựa trên phạm vi mà các thuật toán sử dụng trình vòng lặp làm đầu vào có thể. Ví dụ với std::generate:

Điền vào vùng chứa đến limit(bị loại trừ, limitlà một trình lặp hợp lệ được bật numbers) với các biến từ một phân phối và phần còn lại với các biến từ một phân phối khác.

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

Các thuật toán dựa trên trình lặp lại cho phép bạn kiểm soát tốt hơn phạm vi mà bạn đang hoạt động.


8
Trong khi lý do dễ đọc là LỚN để thích các thuật toán hơn, đây là câu trả lời duy nhất cho thấy vòng lặp dựa trên phạm vi chỉ là một tập hợp con của các thuật toán là gì và do đó không thể phản đối bất cứ điều gì ...
K-bubble

6

Đối với trường hợp cụ thể std::generate, tôi đồng ý với các câu trả lời trước đó về vấn đề khả năng đọc / ý định. std :: create có vẻ là một phiên bản rõ ràng hơn đối với tôi. Nhưng tôi thừa nhận rằng đây là một vấn đề của thị hiếu.

Điều đó nói rằng, tôi có một lý do khác để không vứt bỏ thuật toán std :: - có một số thuật toán chuyên biệt cho một số kiểu dữ liệu.

Ví dụ đơn giản nhất sẽ là std::fill. Phiên bản chung được triển khai dưới dạng vòng lặp trong phạm vi được cung cấp và phiên bản này sẽ được sử dụng khi khởi tạo mẫu. Nhưng không phải lúc nào cũng vậy. Ví dụ: nếu bạn cung cấp cho nó một phạm vi std::vector<int>- thường thì nó sẽ thực sự gọi ẩn memset, mang lại mã nhanh hơn và tốt hơn nhiều.

Vì vậy, tôi đang cố gắng chơi một lá bài hiệu quả ở đây.

Vòng lặp viết tay của bạn có thể nhanh như phiên bản thuật toán std :: nhưng khó có thể nhanh hơn. Và hơn thế nữa, thuật toán std :: có thể chuyên biệt cho các vùng chứa và loại cụ thể và nó được thực hiện dưới giao diện STL sạch.


3

Câu trả lời của tôi sẽ là có thể và không. Nếu chúng ta đang nói về C ++ 11, thì có thể (giống như không). Ví dụ, std::for_eachthực sự khó chịu khi sử dụng ngay cả với lambdas:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
    // do stuff with x
});

Nhưng sử dụng dựa trên phạm vi cho tốt hơn rất nhiều:

for (auto& x : c)
{
    // do stuff with x
}

Mặt khác, nếu chúng ta đang nói về C ++ 1y, thì tôi sẽ tranh luận rằng không, các thuật toán sẽ không bị cản trở bởi phạm vi dựa trên cho. Trong ủy ban tiêu chuẩn C ++, có một nhóm nghiên cứu đang làm việc với đề xuất thêm phạm vi vào C ++ và cũng có công việc đang được thực hiện trên lambdas đa hình. Các dãy sẽ loại bỏ nhu cầu sử dụng cặp biến lặp và lambda đa hình sẽ cho phép bạn không chỉ định loại đối số chính xác của lambda. Điều này có nghĩa là std::for_eachcó thể được sử dụng như thế này (đừng coi đây là một thực tế khó khăn, nó chỉ là những gì những giấc mơ trông giống như ngày hôm nay):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});

Vì vậy, trong trường hợp sau, lợi thế của thuật toán sẽ là bằng cách viết []với lambda mà bạn chỉ định zero-capture? Có nghĩa là, so với việc chỉ viết một phần nội dung vòng lặp, bạn đã tách một đoạn mã khỏi ngữ cảnh tra cứu biến mà nó xuất hiện một cách từ vựng. Việc cô lập thường hữu ích cho người đọc, ít phải suy nghĩ hơn khi đọc.
Steve Jessop

1
Việc nắm bắt không phải là vấn đề. Vấn đề là với lambda đa hình, bạn sẽ không cần phải đánh vần rõ ràng kiểu x là gì.
sdkljhdf hda

1
Trong trường hợp đó, đối với tôi, dường như trong C ++ 1y giả định này, for_eachvẫn vô nghĩa ngay cả khi được sử dụng với lambda. foreach + capture lambda hiện là một cách viết dài dòng cho vòng lặp for dựa trên phạm vi và nó trở nên ít dài dòng hơn một chút nhưng vẫn giống với vòng lặp. Tất nhiên, không phải tôi nghĩ bạn phải bảo vệ for_each, nhưng ngay cả trước khi nhìn thấy câu trả lời của bạn, tôi đã nghĩ rằng nếu người hỏi muốn đánh bại các thuật toán, anh ta có thể chọn for_eachmục tiêu mềm nhất trong tất cả các mục tiêu có thể ;-)
Steve Jessop

Sẽ không bảo vệ for_each, nhưng nó có một lợi thế nhỏ so với dựa trên phạm vi - bạn có thể làm cho nó song song dễ dàng hơn chỉ bằng cách đặt tiền tố cho nó bằng song song_ để biến nó thành parallel_for_each(nếu bạn sử dụng PPL và giả sử nó an toàn để làm như vậy) . :-D
sdkljhdf hda

@lego Lợi thế "nhỏ bé" của bạn thực sự là một lợi thế "lớn" nếu khái quát hóa nó cho đến thực tế, rằng việc std::algorithmtriển khai của nó bị ẩn sau giao diện của chúng và có thể phức tạp tùy ý (hoặc được tối ưu hóa tùy ý).
Christian Rau

1

Một điều cần được chú ý là một thuật toán thể hiện những gì được thực hiện chứ không phải như thế nào.

Vòng lặp dựa trên phạm vi bao gồm cách mọi thứ được thực hiện: bắt đầu với phần tử đầu tiên, áp dụng và chuyển sang phần tử tiếp theo cho đến khi kết thúc. Ngay cả một thuật toán đơn giản cũng có thể làm mọi thứ khác đi (ít nhất là một số quá tải cho các vùng chứa cụ thể, thậm chí không nghĩ đến vectơ kinh khủng), và ít nhất cách nó được thực hiện không phải là việc của người viết.

Đối với tôi đó là phần lớn sự khác biệt, hãy gói gọn hết mức có thể, và điều đó biện minh cho câu khi bạn có thể, hãy sử dụng các thuật toán.


1

Vòng lặp dựa trên phạm vi chỉ có vậy. Tất nhiên cho đến khi tiêu chuẩn được thay đổi.

Thuật toán là một hàm. Một hàm đặt một số yêu cầu đối với các tham số của nó. Các yêu cầu được diễn giải trong một tiêu chuẩn để cho phép thực hiện ví dụ tận dụng tất cả các luồng thực thi có sẵn và sẽ tự động tăng tốc cho bạ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.