Thích các thuật toán cho các vòng lặp viết tay?


10

Điều nào sau đây để bạn thấy dễ đọc hơn? Vòng lặp viết tay:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Hoặc lời gọi thuật toán:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Tôi tự hỏi nếu std::for_eachnó thực sự có giá trị nó, đưa ra một ví dụ đơn giản như vậy đã yêu cầu rất nhiều mã.

Suy nghĩ của bạn về vấn đề này là gì?


2
Đối với C ++, câu trả lời là khá rõ ràng. Nhưng trong các ngôn ngữ khác, cả hai đều bằng nhau (ví dụ: Python:, map(bar.process, vec)mặc dù bản đồ cho các tác dụng phụ không được khuyến khích và các biểu thức hiểu / trình tạo danh sách được khuyến nghị trên bản đồ).

5
Và sau đó cũng có BOOST_FOREACH...
Benjamin Bannier

Vì vậy, mục trong STL hiệu quả không thuyết phục bạn? stackoverflow.com/questions/135129/ hy
Công việc

Câu trả lời:


18

Có một lý do mà lambdas được giới thiệu, và đó là bởi vì ngay cả Standard Commitee cũng nhận ra rằng hình thức thứ hai rất tệ. Sử dụng mẫu đầu tiên, cho đến khi bạn nhận được hỗ trợ C ++ 0x và lambda.


2
Lý do tương tự có thể được sử dụng để biện minh cho hình thức thứ hai: Ủy ban Tiêu chuẩn nhận ra rằng nó vượt trội hơn nhiều đến mức họ giới thiệu lambdas để làm cho nó thậm chí còn tốt hơn. :-p (tuy nhiên +1)
Konrad Rudolph

Tôi không nghĩ rằng thứ hai là xấu. Nhưng nó có thể tốt hơn. Ví dụ, bạn cố tình làm cho đối tượng Bar khó sử dụng hơn bằng cách không thêm toán tử (). Bằng cách sử dụng nó, nó đơn giản hóa rất nhiều điểm gọi trong vòng lặp và làm cho mã gọn gàng.
Martin York

Lưu ý: hỗ trợ lambda đã được thêm vào vì hai khiếu nại lớn. 1) Bản tóm tắt tiêu chuẩn cần thiết để truyền tham số cho functor. 2) Không có mã tại điểm gọi. Mã của bạn thỏa mãn cả hai điều này vì bạn chỉ đang gọi một phương thức. Vì vậy, 1) không có mã bổ sung để truyền tham số 2) Mã đã không ở điểm gọi nên lambda sẽ không cải thiện khả năng duy trì mã. Vì vậy, có lambda là một bổ sung tuyệt vời. Đây không phải là một lý lẽ tốt cho họ.
Martin York

9

Luôn sử dụng biến thể mô tả tốt nhất những gì bạn định làm. Đó là

Đối với mỗi yếu tố xtrong vec, làm bar.process(x).

Bây giờ, hãy xem xét các ví dụ:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Chúng tôi cũng có một cái for_eachđó - yippeh . Chúng tôi có [begin; end)phạm vi chúng tôi muốn hoạt động.

Về nguyên tắc, thuật toán rõ ràng hơn nhiều và do đó thích hợp hơn bất kỳ triển khai viết tay nào. Nhưng sau đó ... Chất kết dính? Memfun? Về cơ bản C ++ interna làm thế nào để có được chức năng thành viên? Đối với nhiệm vụ của tôi, tôi không quan tâm đến họ! Tôi cũng không muốn phải chịu đựng cú pháp dài dòng, rùng rợn này.

Bây giờ khả năng khác:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Cấp, đây là một mô hình phổ biến để nhận ra, nhưng ... tạo các vòng lặp, lặp, tăng, khử hội nghị. Đây cũng là tất cả những điều tôi không quan tâm để hoàn thành nhiệm vụ của mình.

Phải thừa nhận rằng, có vẻ waay tốt hơn so với các giải pháp đầu tiên (ít nhất, vòng cơ thể linh hoạt và khá rõ ràng), nhưng vẫn còn, nó không phải là thực sự tuyệt vời. Chúng tôi sẽ sử dụng cái này nếu chúng tôi không có khả năng tốt hơn, nhưng có lẽ chúng tôi có ...

Một cách tốt hơn?

Bây giờ trở lại for_each. Sẽ không tuyệt vời khi nói theo nghĩa đen for_each linh hoạt trong hoạt động sẽ được thực hiện, quá? May mắn thay, vì C ++ 0x lambdas, chúng tôi

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Bây giờ chúng tôi đã tìm thấy một giải pháp chung chung, trừu tượng cho nhiều tình huống liên quan, đáng chú ý là trong trường hợp cụ thể này, có một yêu thích tuyệt đối số 1 :

foreach(const Foo& x, vec) bar.process(x);

Nó thực sự không thể nhận được rõ ràng hơn nhiều. Rất may, C ++ 0x get của một cú pháp tương tự built-in !


2
Đã nói "cú pháp tương tự" for (const Foo& x : vec) bar.process(x);hoặc sử dụng const auto&nếu bạn muốn.
Jon Purdy

const auto&có khả năng? Không biết rằng - thông tin tuyệt vời!
Dario

8

Bởi vì điều này là không thể đọc được?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}

4
Xin lỗi, nhưng vì một số lý do, nó nói "bị kiểm duyệt" nơi tôi tin rằng mã của bạn phải như vậy.
Mateen Ulhaq

6
Mã của bạn đưa ra một cảnh báo trình biên dịch, bởi vì so sánh một intvới một size_tlà nguy hiểm. Ngoài ra, lập chỉ mục không hoạt động với mọi container, ví dụ std::set.
dòng chảy

2
Mã này sẽ chỉ hoạt động cho các container có toán tử lập chỉ mục, trong đó có rất ít. Nó liên kết thuật toán xuống một cách bất thường - nếu bản đồ <> phù hợp hơn thì sao? Bản gốc cho vòng lặp và for_each sẽ không bị ảnh hưởng.
JBRWilkinson

2
Và bao nhiêu lần bạn thiết kế mã cho một vectơ và sau đó quyết định hoán đổi nó cho bản đồ? Như thể thiết kế cho đó là ưu tiên ngôn ngữ.
Martin Beckett

2
Lặp lại các bộ sưu tập theo chỉ mục không được khuyến khích vì một số bộ sưu tập có hiệu suất kém khi lập chỉ mục, trong khi tất cả các bộ sưu tập hoạt động độc đáo khi sử dụng trình lặp.
slaphappy

3

nói chung, hình thức đầu tiên có thể đọc được bởi khá nhiều người biết vòng lặp for là gì, bất kể nền tảng wat họ có là gì.

nói chung, cái thứ hai hoàn toàn không thể đọc được: đủ dễ để tìm ra những gì for_each đang làm, nhưng nếu bạn chưa bao giờ thấy std::bind1st(std::mem_fun_reftôi có thể tưởng tượng nó thật khó hiểu.


2

Thật ra, ngay cả với C ++ 0x, tôi không chắc for_eachsẽ nhận được nhiều tình yêu.

for(Foo& foo: vec) { bar.process(foo); }

Là cách dễ đọc hơn.

Một điều tôi không thích về các thuật toán (trong C ++) là chúng lập luận trên các trình vòng lặp, tạo ra các câu lệnh rất dài dòng.


2

Nếu bạn đã viết thanh như một functor thì nó sẽ đơn giản hơn rất nhiều:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Ở đây mã khá dễ đọc.


2
Nó khá dễ đọc ngoài thực tế là bạn vừa viết một functor.
Winston Ewert

Xem ở đây để biết lý do tại sao tôi nghĩ rằng mã này là tồi tệ hơn.
sbi

1

Tôi thích cái sau vì nó gọn gàng và sạch sẽ. Nó thực sự là một phần của nhiều ngôn ngữ khác nhưng trong C ++, nó là một phần của thư viện. Nó không thực sự quan trọng.


0

Bây giờ vấn đề này thực sự không cụ thể đối với C ++, tôi sẽ tuyên bố.

Vấn đề là, chuyển một số functor vào một chức năng khác không có nghĩa là để thay thế các vòng lặp .
Nó được cho là đơn giản hóa một số mẫu nhất định, trở nên rất tự nhiên với lập trình chức năng, chẳng hạn như khách truy cậpngười quan sát , chỉ cần đặt tên hai, ngay lập tức tôi nghĩ đến.
Trong các ngôn ngữ thiếu các hàm thứ tự đầu tiên (Java có lẽ là ví dụ tốt nhất), các cách tiếp cận như vậy luôn yêu cầu thực hiện một số giao diện nhất định, khá dài dòng và dư thừa.

Một cách sử dụng phổ biến tôi thấy rất nhiều trong các ngôn ngữ khác sẽ là:

someCollection.forEach(someUnaryFunctor);

Ưu điểm của việc này là, bạn không cần biết someCollectionthực tế được thực hiện như thế nào hoặc làm gì someUnaryFunctor. Tất cả những gì bạn cần biết là, forEachphương thức của nó sẽ lặp lại tất cả các phần tử của bộ sưu tập, chuyển chúng đến hàm đã cho.

Cá nhân, nếu bạn ở vị trí, có tất cả thông tin về cấu trúc dữ liệu bạn muốn lặp lại và tất cả thông tin về những gì bạn muốn làm trong mỗi bước lặp, thì cách tiếp cận chức năng là quá mức các thứ, đặc biệt là trong ngôn ngữ, nơi này xuất hiện khá tẻ nhạt.

Ngoài ra, bạn nên nhớ rằng cách tiếp cận chức năng chậm hơn, bởi vì bạn có rất nhiều cuộc gọi, với chi phí nhất định, mà bạn không có trong vòng lặp for.


1
"Ngoài ra, bạn nên nhớ rằng, cách tiếp cận chức năng chậm hơn, bởi vì bạn có rất nhiều cuộc gọi, với chi phí nhất định, mà bạn không có trong một vòng lặp." ? Tuyên bố này không được hỗ trợ. Cách tiếp cận chức năng không nhất thiết phải chậm hơn, đặc biệt là vì có thể có tối ưu hóa dưới mui xe. Và ngay cả khi bạn hoàn toàn hiểu vòng lặp của mình, nó có thể sẽ trông gọn gàng hơn ở dạng trừu tượng hơn.
Andres F.

@Andres F: Vì vậy, điều này có vẻ sạch hơn? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));Và nó chậm hơn trong thời gian chạy hoặc thời gian biên dịch (nếu bạn may mắn). Tôi không thấy lý do tại sao thay thế các cấu trúc điều khiển luồng bằng các lệnh gọi hàm làm cho mã tốt hơn. Về bất kỳ sự thay thế nào được trình bày ở đây là dễ đọc hơn.
back2dos

0

Tôi nghĩ vấn đề ở đây for_eachkhông thực sự là một thuật toán, nó chỉ là một cách khác (và thường là kém hơn) để viết một vòng lặp viết tay. từ quan điểm này, nó phải là thuật toán tiêu chuẩn ít được sử dụng nhất và có lẽ bạn cũng có thể sử dụng một vòng lặp for. tuy nhiên, lời khuyên trong tiêu đề vẫn tồn tại vì có một số thuật toán tiêu chuẩn phù hợp với việc sử dụng cụ thể hơn.

Trường hợp có một thuật toán chặt chẽ hơn thực hiện những gì bạn muốn thì có, bạn nên ưu tiên các thuật toán hơn các vòng lặp viết tay. các ví dụ rõ ràng ở đây là sắp xếp với sorthoặc stable_sorthoặc tìm kiếm với lower_bound, upper_boundhoặc equal_range, nhưng hầu hết trong số chúng có một số tình huống trong đó chúng được ưu tiên sử dụng hơn một vòng lặp được mã hóa bằng tay.


0

Đối với đoạn mã nhỏ này, cả hai đều có thể đọc được bằng nhau. Nói chung, tôi thấy các vòng lặp thủ công dễ bị lỗi và thích sử dụng các thuật toán, nhưng nó thực sự phụ thuộc vào một tình huống cụ thể.

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.