Tại sao tất cả các hàm <Thuật toán> chỉ lấy các phạm vi, không phải các vùng chứa?


49

Có nhiều hàm hữu ích <algorithm>, nhưng tất cả chúng đều hoạt động theo "trình tự" - cặp trình vòng lặp. Ví dụ, nếu tôi có một container và thích chạy std::accumulatetrên nó, tôi cần phải viết:

std::vector<int> myContainer = ...;
int sum = std::accumulate(myContainer.begin(), myContainer.end(), 0);

Khi tất cả những gì tôi dự định làm là:

int sum = std::accumulate(myContainer, 0);

Đó là một chút dễ đọc và rõ ràng hơn, trong mắt tôi.

Bây giờ tôi có thể thấy rằng có thể có trường hợp bạn chỉ muốn hoạt động trên các bộ phận của một container, vì vậy chắc chắn sẽ rất hữu ích khi có tùy chọn vượt qua phạm vi. Nhưng ít nhất theo kinh nghiệm của tôi, đó là một trường hợp đặc biệt hiếm gặp. Tôi thường sẽ muốn hoạt động trên toàn bộ container.

Thật dễ dàng để viết một hàm bao bọc lấy một container và các cuộc gọi begin()end()trên đó, nhưng các chức năng tiện lợi như vậy không được bao gồm trong thư viện tiêu chuẩn.

Tôi muốn biết lý do đằng sau sự lựa chọn thiết kế STL này.


7
STL thường cung cấp các trình bao bọc tiện lợi, hay nó tuân theo chính sách cũ hơn của C ++ ở đây là các công cụ-bây giờ-đi-tự-bắn-chính mình?
Kilian Foth

2
Đối với bản ghi: thay vì viết trình bao bọc của riêng bạn, bạn nên sử dụng trình bao bọc thuật toán trong Boost.Range; trong trường hợp này,boost::accumulate
ecatmur

Câu trả lời:


40

... chắc chắn rất hữu ích khi có tùy chọn vượt qua phạm vi. Nhưng ít nhất theo kinh nghiệm của tôi, đó là một trường hợp đặc biệt hiếm gặp. Tôi thường muốn hoạt động trên toàn bộ container

Nó có thể là một trường hợp đặc biệt hiếm gặp trong kinh nghiệm của bạn , nhưng trong thực tế, toàn bộ container là trường hợp đặc biệt và phạm vi tùy ý là trường hợp chung.

Bạn đã nhận thấy rằng bạn có thể triển khai toàn bộ thùng chứa bằng giao diện hiện tại, nhưng bạn không thể thực hiện ngược lại.

Vì vậy, người viết thư viện đã có sự lựa chọn giữa việc thực hiện hai giao diện lên phía trước hoặc chỉ thực hiện một giao diện vẫn bao gồm tất cả các trường hợp.


Thật dễ dàng để viết một hàm bao bọc lấy một container và các lệnh gọi start () và end () trên nó, nhưng các hàm tiện lợi như vậy không được bao gồm trong thư viện chuẩn

Đúng, đặc biệt là vì các chức năng miễn phí std::beginstd::endhiện được bao gồm.

Vì vậy, giả sử thư viện cung cấp quá tải tiện lợi:

template <typename Container>
void sort(Container &c) {
  sort(begin(c), end(c));
}

bây giờ nó cũng cần cung cấp quá tải tương đương khi lấy functor so sánh, và chúng ta cần cung cấp các tương đương cho mọi thuật toán khác.

Nhưng ít nhất chúng tôi đã bao gồm mọi trường hợp chúng tôi muốn vận hành trên một container đầy đủ, phải không? Vâng, không hoàn toàn. Xem xét

std::for_each(c.rbegin(), c.rend(), foo);

Nếu chúng ta muốn xử lý hoạt động ngược trên các container, chúng ta cần một phương thức khác (hoặc cặp phương thức) cho mỗi thuật toán hiện có.


Vì vậy, cách tiếp cận dựa trên phạm vi là tổng quát hơn theo nghĩa đơn giản rằng:

  • nó có thể làm mọi thứ mà phiên bản toàn container có thể
  • cách tiếp cận toàn bộ container tăng gấp đôi hoặc gấp ba số lần quá tải cần thiết, trong khi vẫn kém mạnh mẽ hơn
  • các thuật toán dựa trên phạm vi cũng có thể kết hợp được (bạn có thể xếp chồng hoặc xâu chuỗi các bộ điều hợp trình vòng lặp, mặc dù điều này thường được thực hiện trong các ngôn ngữ chức năng và Python)

Tất nhiên, có một lý do hợp lệ khác, đó là đã có rất nhiều công việc để đạt được tiêu chuẩn STL, và thổi phồng nó bằng các trình bao bọc tiện lợi trước khi nó được sử dụng rộng rãi sẽ không sử dụng nhiều thời gian của ủy ban hạn chế. Nếu bạn quan tâm, bạn có thể tìm thấy báo cáo kỹ thuật của Stepanov & Lee tại đây

Như đã đề cập trong các bình luận, Boost.Range cung cấp một cách tiếp cận mới hơn mà không yêu cầu thay đổi tiêu chuẩn.


9
Tôi không nghĩ bất cứ ai, bao gồm OP, đang đề xuất thêm quá tải cho mỗi trường hợp đặc biệt. Ngay cả khi "toàn bộ container" ít phổ biến hơn "phạm vi tùy ý", nó chắc chắn phổ biến hơn nhiều so với "toàn bộ container, đảo ngược". Hạn chế nó f(c.begin(), c.end(), ...)và có lẽ chỉ là quá tải được sử dụng phổ biến nhất (tuy nhiên bạn xác định điều đó) để ngăn chặn việc nhân đôi số lượng quá tải. Ngoài ra, các bộ điều hợp trình lặp hoàn toàn trực giao (như bạn lưu ý, chúng hoạt động tốt trong Python, có các trình vòng lặp hoạt động rất khác nhau và không có hầu hết sức mạnh mà bạn nói đến).

3
Tôi đồng ý toàn bộ container, trường hợp chuyển tiếp là rất phổ biến, nhưng muốn chỉ ra rằng đó là một tập hợp con nhỏ hơn nhiều có thể sử dụng so với câu hỏi được đề xuất. Cụ thể bởi vì sự lựa chọn không phải giữa toàn bộ container và một phần container, mà là giữa toàn bộ container và một phần container, có thể đảo ngược hoặc điều chỉnh theo cách khác. Và tôi nghĩ thật công bằng khi đề xuất rằng độ phức tạp nhận thấy của việc sử dụng bộ điều hợp sẽ lớn hơn, nếu bạn cũng phải thay đổi quá tải thuật toán của mình.
Vô dụng

23
Lưu ý phiên bản container sẽ bao gồm tất cả các trường hợp nếu STL cung cấp một đối tượng phạm vi; ví dụ std::sort(std::range(start, stop)).

3
Ngược lại: các thuật toán chức năng có thể kết hợp (như bản đồ và bộ lọc) lấy một đối tượng đại diện cho một bộ sưu tập và trả về một đối tượng duy nhất, chúng chắc chắn không sử dụng bất cứ thứ gì giống như một cặp trình vòng lặp.
Svick

3
một macro có thể làm điều này: #define MAKE_RANGE(container) (container).begin(), (container).end()</ jk>
ratchet freak

21

Hóa ra có một bài viết của Herb Sutter về chính chủ đề này. Về cơ bản, vấn đề là sự mơ hồ quá tải. Đưa ra những điều sau đây:

template<typename Iter>
void sort( Iter, Iter ); // 1

template<typename Iter, typename Pred>
void sort( Iter, Iter, Pred ); // 2

Và thêm vào như sau:

template<typename Container>
void sort( Container& ); // 3

template<typename Container, typename Pred>
void sort( Container&, Pred ); // 4

Sẽ làm cho nó khó phân biệt 41đúng.

Các khái niệm, như được đề xuất nhưng cuối cùng không được bao gồm trong C ++ 0x, sẽ giải quyết được điều đó và cũng có thể phá vỡ nó bằng cách sử dụng enable_if. Đối với một số thuật toán, nó không có vấn đề gì cả. Nhưng họ đã quyết định chống lại nó.

Bây giờ sau khi đọc tất cả các ý kiến ​​và câu trả lời ở đây, tôi nghĩ rangecác đối tượng sẽ là giải pháp tốt nhất. Tôi nghĩ rằng tôi sẽ xem xét Boost.Range.


1
Chà, sử dụng chỉ là một typename Itercon vịt quá gõ cho một ngôn ngữ nghiêm ngặt. Tôi thích ví dụ template<typename Container> void sort(typename Container::iterator, typename Container::iterator); // 1template<template<class> Container, typename T> void sort( Container<T>&, std::function<bool(const T&)> ); // 4vv (có lẽ sẽ giải quyết vấn đề mơ hồ)
Vlad

@Vlad: Thật không may, điều này sẽ không hoạt động cho các mảng cũ đơn giản, vì không T[]::iteratorcó sẵn. Ngoài ra, trình vòng lặp thích hợp không bắt buộc phải là một kiểu lồng nhau của bất kỳ bộ sưu tập nào, nó đủ để xác định std::iterator_traits.
firegurafiku

@firegurafiku: Chà, mảng rất dễ xảy ra trường hợp đặc biệt với một số thủ thuật TMP cơ bản.
Vlad

11

Về cơ bản là một quyết định di sản. Khái niệm iterator được mô hình hóa trên các con trỏ, nhưng các container không được mô hình hóa trên các mảng. Hơn nữa, vì các mảng khó vượt qua (cần một tham số mẫu không phải là chiều dài, nói chung), thường thì một hàm chỉ có sẵn các con trỏ.

Nhưng vâng, nhìn nhận lại quyết định là sai. Chúng ta sẽ tốt hơn với một đối tượng phạm vi có thể xây dựng từ một trong hai begin/endhoặc begin/length; bây giờ chúng ta có nhiều _nthuật toán hậu tố thay thế.


5

Thêm chúng sẽ giúp bạn không có sức mạnh (bạn đã có thể thực hiện toàn bộ vùng chứa bằng cách gọi .begin().end()chính mình) và nó sẽ thêm một điều nữa vào thư viện phải được chỉ định đúng, được thêm vào thư viện bởi các nhà cung cấp, được kiểm tra, bảo trì, Vân vân.

Nói tóm lại, có lẽ không có vì nó không đáng để duy trì một bộ các mẫu bổ sung chỉ để lưu người dùng toàn bộ container khỏi việc nhập một tham số cuộc gọi chức năng bổ sung.


9
Nó sẽ không mang lại cho tôi sức mạnh, đó là sự thật - nhưng cuối cùng, nó cũng không std::getline, và vẫn vậy, nó nằm trong thư viện. Người ta có thể đi xa hơn để nói rằng các cấu trúc điều khiển mở rộng không có được sức mạnh cho tôi, vì tôi có thể làm mọi thứ chỉ bằng ifgoto. Yeah, so sánh không công bằng, tôi biết;) Tôi nghĩ rằng tôi có thể hiểu được / thực hiện / gánh nặng bảo trì đặc điểm kỹ thuật bằng cách nào đó, nhưng nó chỉ là một wrapper nhỏ chúng ta đang nói về đây, vì vậy ..
gây chết người-đàn guitar

Một trình bao bọc nhỏ không tốn gì để viết mã và có lẽ nó không có ý nghĩa gì khi ở trong thư viện.
ebasconp

-1

Đến bây giờ, http://en.wikipedia.org/wiki/C++11#Range-basing_for_loop là một lựa chọn tốt để std::for_each. Quan sát, không có trình lặp rõ ràng:

int a[5] = {1, 2, 3, 4, 5};
for (auto &i: a) { i *= 2; }

(Lấy cảm hứng từ https://stackoverflow.com/a/694534/2097284 .)


1
Nó chỉ giải quyết được phần duy nhất đó <algorithm>, không phải tất cả các thuật toán thực sự cần beginvà các endtrình lặp - nhưng lợi ích không thể bị cường điệu! Khi tôi lần đầu tiên dùng thử C ++ 03 vào năm 2009, tôi đã tránh xa các trình vòng lặp do sự sôi nổi của vòng lặp, và may mắn là không, các dự án của tôi tại thời điểm đó đã cho phép điều này. Khởi động lại trên C ++ 11 vào năm 2014, đó là một bản nâng cấp đáng kinh ngạc, ngôn ngữ C ++ luôn luôn nên có và bây giờ tôi không thể sống thiếu auto &it: them:)
underscore_d
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.