C ++ Iterator, чому немає базового класу Iterator, від якого успадковуються всі ітератори


11

Я навчаюсь на іспиті, і у мене є питання, на яке я намагаюся дати і відповісти.

Чому не існує базового класу ітератора, від якого успадковуються всі інші ітератори?

Я думаю, мій учитель посилається на ієрархічну структуру з посилання на cpp " http://prntscr.com/mgj542 ", і ми повинні надати іншу причину, ніж чому вони повинні?

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

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


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

5
" Чому не існує базового класу ітераторів, від якого успадковуються всі інші ітератори? " Гм ... чому він повинен бути таким?
Нікол

Відповіді:


14

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

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

Розглянемо, наприклад, просту ієрархію ітераторів, яка використовує успадкування та віртуальні функції:

template <class T>
class iterator_base { 
public:
    virtual T &operator*() = 0;
    virtual iterator_base &operator++() = 0;
    virtual bool operator==(iterator_base const &other) { return pos == other.pos; }
    virtual bool operator!=(iterator_base const &other) { return pos != other.pos; }
    iterator_base(T *pos) : pos(pos) {}
protected:
    T *pos;
};

template <class T>
class array_iterator : public iterator_base<T> {
public: 
    virtual T &operator*() override { return *pos; }
    virtual array_iterator &operator++() override { ++pos; return *this; }
    array_iterator(T *pos) : iterator_base(pos) {}
};

Тоді давайте швидкий тест:

int main() { 
    char input[] = "asdfasdfasdfasdfasdfasdfasdfadsfasdqwerqwerqwerqrwertytyuiyuoiiuoThis is a stringy to search for something";
    using namespace std::chrono;

    auto start1 = high_resolution_clock::now();
    auto pos = std::find(std::begin(input), std::end(input), 'g');
    auto stop1 = high_resolution_clock::now();

    std::cout << *++pos << "\n";

    auto start2 = high_resolution_clock::now();
    auto pos2 = std::find(array_iterator(input), array_iterator(input+sizeof(input)), 'g');
    auto stop2 = high_resolution_clock::now();

    std::cout << *++pos2 << "\n";

    std::cout << "time1: " << duration_cast<nanoseconds>(stop1 - start1).count() << "ns\n";
    std::cout << "time2: " << duration_cast<nanoseconds>(stop2 - start2).count() << "ns\n";
}

[Примітка: залежно від вашого компілятора, вам може знадобитися зробити трохи більше, наприклад, визначити iterator_category, type_type, reference тощо, щоб компілятор прийняв ітератор.]

А вихід:

y
y
time1: 1833ns
time2: 2933ns

[Звичайно, якщо ви запустите код, ваші результати точно не відповідають цим.]

Таким чином, навіть для цього простого випадку (і робимо лише близько 80 кроків та порівнянь) ми додали близько 60% накладних витрат до простого лінійного пошуку. Особливо, коли ітератори спочатку були додані до C ++, досить багато людей просто не прийняли б дизайн з такою значною витратою. Вони, ймовірно, не були б стандартизовані, і навіть якби вони були, практично ніхто не використовував би їх.


7

Різниця між тим, що щось є, і тим, як щось поводиться.

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

Якщо як що, а що як ...

Якщо все успадковується від objectцього, то виникають певні переваги, такі як: будь-яка змінна об'єкт може мати будь-яке значення будь-коли. Але це теж руб, все повинно вести себе ( як ) як object, і бути схожим на ( що ) object.

Але:

  • Що робити, якщо ваш об’єкт не має змістовного визначення рівності?
  • Що робити, якщо він не має значущого хешу?
  • Що робити, якщо ваш об’єкт неможливо клонувати, але об’єкти можуть бути?

Або objectтип стає по суті марним - через об'єкт, що не забезпечує спільності у всіх можливих випадках. Або існуватимуть об'єкти, які мають зламане / взуттєве / абсурдне визначення деякої імовірної універсальної властивості, знайденої на objectяких свідчить майже про універсальну поведінку, за винятком ряду гатчів.

Якщо Що не пов'язане з Як

По черзі можна зберігати розділення Що і як . Тоді декілька різних типів (у них немає нічого спільного взагалі що ) можуть поводитись так само, як видно у співпрацівача як . У цьому сенсі ідея створення Iteratorне є специфічним , що , а як . Зокрема, як ви взаємодієте з річчю, коли ви ще не знаєте, з чим спілкуєтесь.

Java (і подібні) дозволяють підходи до цього за допомогою інтерфейсів. Інтерфейс у зв'язку з цим описує засоби зв'язку та неявно протокол зв'язку та дії, який слід дотримуватися. Будь-яке Що , який заявляє про себе , щоб бути даністю Як , йдеться , що він підтримує відповідні зв'язки і дії , викладені в протоколі. Це дозволяє будь-якому Співавтор покладатися на Хау і не загрузнути, вказавши , які саме Які «s можуть бути використані.

C ++ (і подібні) дозволяють підходити до цього шляхом натискання качки. Шаблон не має значення, якщо тип співпраці заявляє, що він слід за поведінкою, а саме в даному контексті компіляції, з якою об'єкт може взаємодіяти певним чином. Це дозволяє вказівникам C ++ та конкретним операторам, що пересувають об'єкти, використовувати той самий код. Тому що вони відповідають контрольному списку, щоб вважати його рівнозначним.

  • підтримує * a, a->, ++ a і ++ -> ітератор вводу / переадресації
  • підтримує * a, a->, ++ a, a ++, --a та a-- -> двонаправлений ітератор

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

Під і над специфікаціями

Проблема обох підходів полягає в недостатній специфікації.

Використання інтерфейсу вимагає, щоб об'єкт оголосив, що він підтримує задану поведінку, що також означає, що творець повинен перейняти це спочатку. Це призводить до того, що деякі What 's не роблять розріз, оскільки вони не заявляли про це. Це також означає, що коли-небудь, що має спільного предка, інтерфейс, що представляє Як . Це повертається до початкової проблеми object. Це змушує співпрацівників переоцінювати свої вимоги, в той же час спричиняючи, що деякі об'єкти є або непридатними через відсутність декларації, або приховані дітки, оскільки очікувана поведінка погано визначена.

Використання шаблону вимагає, щоб співпрацівник працював із абсолютно невідомим Що , і завдяки його взаємодії визначав як . В якійсь мірі це ускладнює написання співпрацівників, оскільки він повинен аналізувати що для своїх примітивних комунікацій (функції / поля / тощо), уникаючи помилок компіляції або, принаймні, вказувати на те, як дане те, що не відповідає його вимогам щодо " Як" . Це дозволяє співробітнику вимагати абсолютного мінімуму від будь-якого даного What , що дозволяє найширшому діапазону того, що використовується. На жаль, це має і недолік використання безглуздого використання об'єктів, які технічно забезпечують комунікативні примітиви для даної задачіЯк , але не слідкуйте за прихованим протоколом, що дозволяє всілякі погані речі.

Ітератори

У цьому випадку а Iterator- це як це скорочення для опису взаємодії. Все, що відповідає цьому опису, за визначенням є Iterator. Знаючи як дозволяє нам писати загальні алгоритми та мати короткий список " Як дано конкретний Що ", що потрібно надати для того, щоб алгоритм працював. Цей список є функції / властивості / і т.д., їх реалізація враховує специфіку Що , що слухалася алгоритмом.


6

Тому що для C ++ не потрібно мати (абстрактних) базових класів для поліморфізму. Він має структурну підтипізацію , а також номінативний підтип .

Конфуз у конкретному випадку Ітераторів, попередні стандарти визначалися std::iteratorяк (приблизно)

template <class Category, class T, class Distance = std::ptrdiff_t, class Pointer = T*, class Reference = T&>
struct iterator {
    using iterator_category = Category;
    using value_type = T;
    using difference_type = Distance;
    using pointer = Pointer;
    using reference = Reference;
}

Тобто просто постачальник необхідних типів членів. Він не мав жодної поведінки під час виконання, і був застарілий у C ++ 17

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



5

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.