Чому всі функції <алгоритму> приймають лише діапазони, а не контейнери?


49

У програмах є багато корисних функцій <algorithm>, але всі вони працюють на «послідовностях» - парах ітераторів. Наприклад, якщо я маю контейнер і люблю бігати std::accumulateпо ньому, мені потрібно написати:

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

Коли все, що я маю намір зробити:

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

Що в моїх очах трохи читабельніше і зрозуміліше.

Тепер я бачу, що можуть бути випадки, коли ви хочете оперувати лише частинами контейнера, тому, безумовно, корисно мати можливість проходження діапазонів. Але, принаймні, з мого досвіду, це рідкісний особливий випадок. Зазвичай я хочу оперувати цілими контейнерами.

Написати функцію обгортки, яка приймає контейнер, дзвонить begin()і end()на нього легко, але такі зручні функції не включені до стандартної бібліотеки.

Мені хотілося б дізнатися обґрунтування цього вибору дизайну STL.


7
Чи STL, як правило, надає зручні обгортки, чи він дотримується старішої C ++ ось тут - інструменти-зараз-іди-знімай-себе-в-ногу?
Кіліан Фот

2
Для запису: замість того, щоб писати власну обгортку, слід використовувати обгортки алгоритму в Boost.Range; у цьому випадкуboost::accumulate
ecatmur

Відповіді:


40

... напевно корисно мати можливість проходження діапазонів. Але, принаймні, з мого досвіду, це рідкісний особливий випадок. Зазвичай я хочу оперувати цілими контейнерами

Це може бути рідкісним особливим випадком у вашому досвіді , але насправді весь контейнер - це особливий випадок, а довільний діапазон - загальний випадок.

Ви вже помітили, що ви можете реалізувати весь корпус контейнера за допомогою поточного інтерфейсу, але не можете зробити зворотне.

Отже, у бібліотечного письменника був вибір між реалізацією двох інтерфейсів на передній частині, або лише тим, хто реалізує, який все ще охоплює всі випадки.


Легко написати функцію обгортки, яка бере контейнер і викликає на ньому запуск () і закінчення (), але такі зручні функції не включені до стандартної бібліотеки

Правда, тим більше, що безкоштовні функції std::beginі std::endзараз включені.

Скажімо, бібліотека забезпечує зручне перевантаження:

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

тепер він також повинен забезпечити еквівалентну перевантаження, приймаючи функцію порівняння, і нам потрібно надати еквіваленти для кожного іншого алгоритму.

Але ми принаймні висвітлювали кожен випадок, коли ми хочемо працювати на повному контейнері, правда? Ну, не зовсім. Розглянемо

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

Якщо ми хочемо , щоб впоратися з операційними назад на контейнерах, нам потрібна інший метод (або пару методів) в відповідно з існуючим алгоритмом.


Отже, підхід на основі дальності є більш загальним у простому розумінні, що:

  • він може зробити все, що може використовувати версія для контейнерів
  • підхід із цілим контейнером подвоює або втричі збільшує кількість необхідних перевантажень, залишаючись при цьому менш потужними
  • алгоритми на основі діапазону також є композиційними (ви можете складати або ланцюжок адаптерів ітератора, хоча це частіше робиться у функціональних мовах та Python)

Звичайно, є ще одна вагома причина, яка полягає в тому, що для того, щоб стандартизувати STL, було вже багато роботи, і надути його зручними обгортками до його широкого використання не було б великим використанням обмеженого часу комітету. Якщо вас цікавить, ви можете знайти технічний звіт компанії Stepanov & Lee тут

Як зазначалося в коментарях, Boost.Range забезпечує новіший підхід, не вимагаючи змін до стандарту.


9
Я не думаю, що ніхто, включаючи ОП, пропонує додавати перевантаження для кожного окремого випадку. Навіть якщо "весь контейнер" був менш поширеним, ніж "довільний діапазон", він, безумовно, набагато частіше, ніж "весь контейнер, перевернутий". Обмежте це f(c.begin(), c.end(), ...), можливо, і найпоширенішим перевантаженням (однак ви це визначите), щоб запобігти подвоєнню кількості перевантажень. Крім того, ітераторні адаптери є повністю ортогональними (як зазначаєте, вони добре працюють у Python, чиї ітератори працюють дуже по-різному і не мають більшої потужності, про яку ви говорите).

3
Я погоджуюсь з цілим контейнером, випадок форвардів є дуже поширеним, але хотів би зазначити, що це набагато менший набір можливих застосувань, ніж запропоновано питання. Зокрема, тому що вибір не між цілим та частковим контейнером, а між цілим та частковим контейнером, можливо, перевернутим або іншим чином адаптованим. І я думаю, що справедливо припустити, що сприйнята складність використання адаптерів більша, якщо вам також доведеться змінити перевантаження алгоритму.
Марно

23
Зверніть увагу, що версія контейнера охоплювала б усі випадки, якщо STL пропонував об'єкт діапазону; напр std::sort(std::range(start, stop)).

3
Навпаки: складові функціональні алгоритми (наприклад, карта та фільтр) беруть один об'єкт, який представляє колекцію та повертає один об'єкт, вони, звичайно, не використовують нічого, що нагадує пару ітераторів.
svick

3
макрос міг би це зробити: #define MAKE_RANGE(container) (container).begin(), (container).end()</jk>
храповик урод

21

Виявляється, є стаття Герба Саттера на цю саму тему. В основному проблема полягає в неоднозначності перевантаження. З огляду на наступне:

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

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

І додаючи наступне:

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

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

Буде важко розрізнити 4і 1правильно.

Концепції, запропоновані, але, зрештою, не включені до C ++ 0x, вирішили б це, і також можна обійти його за допомогою enable_if. Для деяких алгоритмів це взагалі не проблема. Але вони вирішили проти цього.

Тепер, прочитавши всі коментарі та відповіді тут, я думаю, що rangeоб'єкти були б найкращим рішенням. Думаю, я подивлюсь Boost.Range.


1
Що ж, використання суто мови, typename Iterздається, занадто напечене для суворої мови. Я вважаю за краще, наприклад, template<typename Container> void sort(typename Container::iterator, typename Container::iterator); // 1і template<template<class> Container, typename T> void sort( Container<T>&, std::function<bool(const T&)> ); // 4т. Д. (Що, можливо, вирішить проблему неоднозначності)
Влад

@Vlad: На жаль, це не працюватиме для простих старих масивів, оскільки їх немає T[]::iterator. Крім того, належний ітератор не зобов'язаний бути вкладеним типом будь-якої колекції, достатньо визначитися std::iterator_traits.
firegurafiku

@firegurafiku: Ну, масиви легко провести в окремих випадках з деякими основними хитрощами TMP.
Влад

11

В основному спадкове рішення. Концепція ітератора моделюється вказівниками, але контейнери не моделюються на масивах. Крім того, оскільки масиви важко пропустити (взагалі потрібен параметр шаблону нетипового шаблону для довжини, як правило), часто функція має лише покажчики.

Але так, в огляді рішення невірно. Нам краще було б об'єкт діапазону, який можна сконструювати з будь-якого begin/endабо begin/length; Тепер _nнатомість у нас є кілька суфіксальних алгоритмів.


5

Додавання їх не дасть вам ніякої сили (ви вже можете зробити весь контейнер, зателефонувавши .begin()і .end()самі), і це додало б ще одну річ у бібліотеку, яку слід правильно вказати, додавати в бібліотеки постачальниками, перевіряти, підтримувати, тощо.

Коротше кажучи, його, мабуть, немає, тому що не варто намагатися підтримувати набір додаткових шаблонів лише для того, щоб врятувати користувачів цілих контейнерів від введення одного додаткового параметра виклику функції.


9
Це не могло б отримати мою силу, це правда - але врешті-решт, і не std::getline, і все-таки це в бібліотеці. Можна сказати, що розширені структури управління не отримують мене, оскільки я можу робити все, використовуючи лише ifі goto. Так, несправедливе порівняння, я знаю;) Я думаю, я можу якось зрозуміти специфікацію / впровадження / технічне обслуговування, але це лише крихітна обгортка, про яку ми тут говоримо, тому ..
летальна гітара

Крихітна обгортка нічого не коштує кодувати, і, можливо, не має сенсу бути в бібліотеці.
ebasconp

-1

На сьогоднішній день http://en.wikipedia.org/wiki/C++11#Range-based_for_loop є приємною альтернативою std::for_each. Зауважте, немає явних ітераторів:

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

(Натхненно https://stackoverflow.com/a/694534/2097284 .)


1
Він вирішує лише ту <algorithm>саму частину , не всі фактичні альги, які потрібні beginта endітератори - але користь не можна завищувати! Коли я вперше спробував C ++ 03 у 2009 році, я ухилився від ітераторів через кольчувальний цикл, і, на щастя чи ні, мої проекти на той час це дозволяли. Перезапустивши C ++ 11 у 2014 році, це було неймовірним оновленням, мова C ++ завжди повинна була бути, і тепер я не можу жити без auto &it: them:)
underscore_d
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.