C ++ Iterator, Por que não há uma classe base do Iterator que todos os iteradores herdam


11

Estou aprendendo para um exame e tenho uma pergunta que estou lutando para responder e responder.

Por que não existe uma classe base de iterador que todos os outros iteradores herdam?

Meu palpite é que meu professor está se referindo à estrutura hierárquica da referência do cpp " http://prntscr.com/mgj542 " e precisamos fornecer um outro motivo que não seja o porquê.

Eu sei o que são os iteradores (mais ou menos) e que eles são usados ​​para trabalhar em contêineres. Pelo que entendi por causa das diferentes estruturas de dados subjacentes possíveis, contêineres diferentes têm iteradores diferentes, porque você pode acessar aleatoriamente uma matriz, por exemplo, mas não uma lista vinculada e contêineres diferentes exigem maneiras diferentes de se mover por elas.

Provavelmente são modelos especializados, dependendo do contêiner, certo?


2
Compartilhar sua pesquisa ajuda a todos . Conte-nos o que você tentou e por que ele não atendeu às suas necessidades. Isso demonstra que você dedicou um tempo para tentar ajudar a si mesmo, evita reiterar respostas óbvias e, acima de tudo, ajuda a obter uma resposta mais específica e relevante. Veja também How to Ask
gnat

5
" Por que não existe uma classe base de iterador que todos os outros iteradores herdam? " Um ... por que deveria haver um?
Nicol Bolas

Respostas:


14

Você já obteve respostas apontando por que não é necessário que todos os iteradores herdem de uma única classe base do Iterator. Eu tinha ido um pouco mais longe. Um dos objetivos do C ++ é a abstração com custo zero em tempo de execução.

Se os iteradores trabalharam por todos eles herdando de uma classe base comum e usaram funções virtuais na classe base para definir a interface, e as classes derivadas forneceram implementações dessas funções virtuais, isso poderia (e geralmente acrescentaria) tempo de execução substancial despesas gerais para as operações envolvidas.

Vamos considerar, por exemplo, uma hierarquia simples de iterador que usa herança e funções virtuais:

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) {}
};

Então vamos fazer um teste rápido:

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";
}

[note: dependendo do seu compilador, talvez você precise fazer um pouco mais, como definir a categoria do iterador, o tipo de diferença, a referência e assim por diante, para que o compilador aceite o iterador.]

E a saída é:

y
y
time1: 1833ns
time2: 2933ns

[Obviamente, se você executar o código, seus resultados não corresponderão exatamente a eles.]

Portanto, mesmo neste caso simples (e fazendo apenas cerca de 80 incrementos e comparações), adicionamos 60% de sobrecarga a uma pesquisa linear simples. Especialmente quando os iteradores foram adicionados originalmente ao C ++, muitas pessoas simplesmente não teriam aceitado um design com tanta sobrecarga. Eles provavelmente não teriam sido padronizados e, mesmo que tivessem, praticamente ninguém os usaria.


7

A diferença está entre o que é algo e como se comporta.

Muitas línguas tentam confundir as duas, mas são coisas bem distintas.

Se como é o que e como é ...

Se tudo herda object, alguns benefícios ocorrem como: qualquer variável de objeto pode ter qualquer valor. Mas esse também é o problema, tudo deve se comportar ( como ) como um objecte parecer com ( o que ) um object.

Mas:

  • E se o seu objeto não tiver uma definição significativa de igualdade?
  • E se ele não tiver um hash significativo?
  • E se o seu objeto não puder ser clonado, mas os objetos puderem?

Ou o objecttipo se torna essencialmente inútil - devido ao objeto não fornecer elementos comuns em todas as instâncias possíveis. Ou existirão objetos que tenham uma definição quebrada / com chifres / absurdo de alguma propriedade universal presumida encontrada na objectqual prove um comportamento quase universal, exceto por uma série de truques.

Se o que não está relacionado a como

Como alternativa, você pode manter os itens O que e Como separados. Então, vários tipos diferentes (sem nada em comum, tudo o que ) podem se comportar da mesma maneira que o colaborador vê como . Nesse sentido, a idéia de um Iteratornão é um quê específico , mas um como . Especificamente Como você interage com uma coisa quando ainda não sabe com o que está interagindo.

Java (e similares) permitem abordagens para isso usando interfaces. Uma interface a esse respeito descreve os meios de comunicação e, implicitamente, um protocolo de comunicação e ação que é seguido. Qualquer O que se declara de um determinado Como , declara que apóia a comunicação e a ação relevantes descritas no protocolo. Isso permite que qualquer colaborador confie no Como e não fique atolado, especificando exatamente quais O que pode ser usado.

C ++ (e similares) permitem abordagens para isso digitando duck. Um modelo não se importa se o tipo colaborador declara que segue um comportamento, apenas dentro de um determinado contexto de compilação, com o qual o objeto pode ser interagido de uma maneira específica. Isso permite que ponteiros C ++ e objetos substituindo operadores específicos sejam usados ​​pelo mesmo código. Porque eles atendem à lista de verificação para serem considerados equivalentes.

  • suporta * a, a->, ++ ae ++ -> iterador de entrada / encaminhamento
  • suporta * a, a->, ++ a, a ++, --a e a-- -> iterador bidirecional

O tipo subjacente nem precisa estar iterando um contêiner, pode ser qualquer o que . Além disso, permite que alguns colaboradores sejam ainda mais genéricos, imagine que uma função precise apenas a++, um iterador pode satisfazer isso, assim como um ponteiro, um número inteiro, assim como qualquer objeto implementado operator++.

Sob e sobre a especificação

O problema com as duas abordagens está abaixo e acima da especificação.

O uso de uma interface exige que o objeto declare que suporta um determinado comportamento, o que também significa que o criador deve imbuí-lo desde o início. Isso faz com que alguns O que não façam o corte, pois não o declararam. Isso também significa que sempre O que tem um ancestral comum, a interface que representa o Como . Isso volta ao problema inicial de object. Isso faz com que os colaboradores superexcliquem seus requisitos, enquanto simultaneamente fazem com que alguns objetos sejam inutilizáveis ​​devido à falta de declaração ou ocultem truques, pois o comportamento esperado é mal definido.

O uso de um modelo exige que o colaborador trabalhe com um quê completamente desconhecido e, por meio de suas interações, ele define um como . Até certo ponto, isso dificulta a escrita dos colaboradores, pois ele deve analisar o quê para suas primitivas de comunicação (funções / campos / etc), evitando erros de compilação, ou pelo menos apontar como um determinado que não corresponde aos seus requisitos para o Como . Isso permite que o colaborador para exigir o mínimo absoluto de qualquer dado que , permitindo a mais ampla gama do que é para ser usado. Infelizmente, isso tem a desvantagem de permitir usos sem sentido de objetos que fornecem as primitivas de comunicação para um dadoComo , mas não siga o protocolo implícito, permitindo que todos os tipos de coisas ruins ocorram.

Iteradores

Neste caso, um Iteratoré uma Como ela é uma abreviação para uma descrição de interação. Qualquer coisa que corresponda a essa descrição é, por definição, umIterator . Saber como nos permite escrever algoritmos gerais e ter uma pequena lista de ' Como é fornecido um ' O ' específico ' que precisa ser fornecido para que o algoritmo funcione. Essa lista é a função / propriedades / etc, sua implementação leva em consideração o específico O que está sendo tratado pelo algoritmo.


6

Porque o C ++ não precisa ter classes base (abstratas) para executar o polimorfismo. Tem subtipos estrutural , bem como subtipos nominativas .

Confusamente, no caso particular dos Iteradores, os padrões anteriores definidos std::iteratorcomo (aproximadamente)

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;
}

Ou seja, apenas como um provedor dos tipos de membros necessários. Não teve nenhum comportamento em tempo de execução e foi preterido no C ++ 17

Observe que mesmo isso não pode ser uma base comum, pois um modelo de classe não é uma classe, cada instanciação é independente das outras.



5

Uma razão é que os iteradores não precisam ser instâncias de uma classe. Os ponteiros são perfeitamente bons iteradores em muitos casos, por exemplo, e como esses são primitivos, não podem herdar de nada.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.