Não herdarás de std :: vector

189

Ok, isso é realmente difícil de confessar, mas eu tenho uma forte tentação no momento de herdar std::vector.

Preciso de cerca de 10 algoritmos personalizados para vetor e quero que eles sejam diretamente membros do vetor. Mas, naturalmente, também quero ter o restante da std::vectorinterface. Bem, minha primeira idéia, como cidadão cumpridor da lei, era ter um std::vectormembro da MyVectorclasse. Mas então eu teria que reprovar manualmente toda a interface do std :: vector. Demais para digitar. Em seguida, pensei em herança privada, para que, em vez de reprovar métodos, eu escrevesse um monte de using std::vector::member's na seção pública. Isso é tedioso também, na verdade.

E aqui estou eu, realmente acho que posso simplesmente herdar publicamente std::vector, mas forneço um aviso na documentação de que essa classe não deve ser usada polimorficamente. Eu acho que a maioria dos desenvolvedores é competente o suficiente para entender que isso não deve ser usado polimorficamente de qualquer maneira.

Minha decisão é absolutamente injustificável? Se sim, por quê? Você pode fornecer uma alternativa que teria membros adicionais na verdade membros, mas não envolveria redigitar toda a interface do vetor? Duvido, mas se puder, serei feliz.

Além disso, além do fato de que algum idiota pode escrever algo como

std::vector<int>* p  = new MyVector

existe algum outro perigo realista no uso do MyVector? Ao dizer realista, descarto coisas como imaginar uma função que leva um ponteiro para o vetor ...

Bem, eu declarei o meu caso. Eu pequei. Agora cabe a você me perdoar ou não :)

Armen Tsirunyan
fonte
9
Então, você está basicamente perguntando se não há problema em violar uma regra comum com base no fato de que você está com preguiça de reimplementar a interface do contêiner? Então não, não é. Veja, você pode ter o melhor dos dois mundos se engolir a pílula amarga e fazê-la corretamente. Não seja esse cara. Escreva um código robusto.
Jim Brissom
7
Por que você não pode / não deseja adicionar a funcionalidade necessária com funções que não são membros? Para mim, isso seria a coisa mais segura a se fazer nesse cenário.
Simone
11
std::vectorA interface do @ Jim: é bastante grande e, quando o C ++ 1x aparecer, ele se expandirá bastante. É muito para digitar e mais para expandir em alguns anos. Penso que esta é uma boa razão para considerar herança em vez de contenção - se alguém seguir a premissa de que essas funções devem ser membros (o que duvido). A regra para não derivar de contêineres STL é que eles não são polimórficos. Se você não os estiver usando dessa maneira, não se aplicará.
S4
9
A verdadeira questão da pergunta está na frase: "Eu quero que eles sejam membros diretamente do vetor". Nada mais na questão realmente importa. Por que voce quer isso? Qual é o problema de apenas fornecer essa funcionalidade como não-membro?
jalf
8
@ JoshC: "Tu deves" sempre foi mais comum do que "tu deves", e também é a versão encontrada na Bíblia King James (que é geralmente o que as pessoas estão fazendo alusão quando escrevem "tu não [...] "). O que na Terra o levaria a chamá-lo de "erro de ortografia"?
Ruakh

Respostas:

155

Na verdade, não há nada errado com a herança pública de std::vector. Se você precisar, faça isso.

Eu sugeriria fazer isso apenas se for realmente necessário. Somente se você não puder fazer o que deseja com funções gratuitas (por exemplo, deve manter algum estado).

O problema é que MyVectoré uma nova entidade. Isso significa que um novo desenvolvedor de C ++ deve saber o que diabos é antes de usá-lo. Qual é a diferença entre std::vectore MyVector? Qual é o melhor para usar aqui e ali? E se eu precisar mover std::vectorpara MyVector? Posso apenas usar swap()ou não?

Não produza novas entidades apenas para fazer algo parecer melhor. Essas entidades (especialmente, tão comuns) não vão viver no vácuo. Eles viverão em ambiente misto com entropia constantemente aumentada.

Stas
fonte
7
Meu único contra-argumento é que é preciso saber realmente o que ele está fazendo para fazer isso. Por exemplo, não introduza membros de dados adicionais MyVectore tente transmiti-lo para funções que aceitam std::vector&ou std::vector*. Se houver algum tipo de atribuição de cópia envolvida no uso de std :: vector * ou std :: vector &, teremos problemas de divisão em que os novos membros de dados MyVectornão serão copiados. O mesmo seria válido para chamar swap através de um ponteiro / referência base. Costumo pensar que qualquer tipo de hierarquia de herança que corre o risco de cortar objetos é ruim.
stinky472
13
std::vectordestruidor de não é virtual , portanto você nunca deve herdar dele
André Fratelli
2
Criei uma classe que herdou publicamente std :: vector por esse motivo: eu tinha um código antigo com uma classe de vetor não STL e queria passar para STL. Reimplementei a classe antiga como uma classe derivada de std :: vector, permitindo-me continuar usando os nomes de funções antigas (por exemplo, Count () em vez de size ()) no código antigo, enquanto escrevia um novo código usando o std :: vector funções. Eu não adicionei nenhum membro de dados, portanto, o destruidor do std :: vector funcionou bem para objetos criados no heap.
Graham Asher
3
@GrahamAsher Se você excluir um objeto através de um ponteiro para a base e o destruidor não for virtual, seu programa exibirá um comportamento indefinido. Um possível resultado de um comportamento indefinido é "funcionou bem nos meus testes". Outra é que ele envia à sua avó o histórico de navegação na web. Ambos são compatíveis com o padrão C ++. Mudar de um para o outro com versões pontuais de compiladores, sistemas operacionais ou a fase da lua também é compatível.
Yakk - Adam Nevraumont 11/06/19
2
@GrahamAsher Não, sempre que você excluir qualquer objeto por meio de um ponteiro para a base sem um destruidor virtual, isso é um comportamento indefinido sob o padrão. Eu entendo o que você acha que está acontecendo; você acabou de estar errado. "o destruidor da classe base é chamado e funciona" é um sintoma possível (e o mais comum) desse comportamento indefinido, porque esse é o código de máquina ingênuo que o compilador geralmente gera. Isso não o torna seguro nem uma ótima idéia.
Yakk - Adam Nevraumont 11/11/19
92

Todo o STL foi projetado de forma que algoritmos e contêineres sejam separados .

Isso levou a um conceito de diferentes tipos de iteradores: iteradores const, iteradores de acesso aleatório etc.

Portanto, recomendo que você aceite esta convenção e projete seus algoritmos de forma que eles não se importem com o contêiner em que estão trabalhando - e precisariam apenas de um tipo específico de iterador para o qual precisariam executar suas tarefas. operações.

Além disso, deixe-me redirecioná-lo para algumas boas observações de Jeff Attwood .

Kos
fonte
63

O principal motivo para não herdar std::vectorpublicamente é a ausência de um destruidor virtual que efetivamente o impeça de usar polimorficamente os descendentes. Em particular, você não tem permissão paradelete um std::vector<T>*que realmente aponte para um objeto derivado (mesmo que a classe derivada não adicione membros), mas o compilador geralmente não pode avisá-lo sobre isso.

A herança privada é permitida sob essas condições. Por isso, recomendo usar a herança privada e encaminhar os métodos necessários do pai, como mostrado abaixo.

class AdVector: private std::vector<double>
{
    typedef double T;
    typedef std::vector<double> vector;
public:
    using vector::push_back;
    using vector::operator[];
    using vector::begin;
    using vector::end;
    AdVector operator*(const AdVector & ) const;
    AdVector operator+(const AdVector & ) const;
    AdVector();
    virtual ~AdVector();
};

Você deve primeiro refatorar seus algoritmos para abstrair o tipo de contêiner em que eles estão operando e deixá-los como funções de modelo livre, conforme indicado pela maioria dos respondentes. Isso geralmente é feito fazendo um algoritmo aceitar um par de iteradores em vez de contêiner como argumentos.

Basilevs
fonte
IIUC, a ausência de um destruidor virtual é apenas um problema se a classe derivada alocar recursos que devem ser liberados após a destruição. (Eles não seriam liberados em um caso de uso polimórfico porque um contexto que, sem saber, se apropria de um objeto derivado via ponteiro para a base chamaria apenas o destruidor da base quando chegar a hora.) Problemas semelhantes surgem de outras funções-membro substituídas, portanto, é necessário cuidado pode-se considerar que os base são válidos para chamar. Mas, na falta de recursos adicionais, existem outros motivos?
Peter - Restabelece Monica
2
vectorO armazenamento alocado do não é o problema - afinal, vectoro destruidor do sistema seria chamado corretamente através de um ponteiro para vector. É justo que os proíbe padrão deleteing loja livre objetos através de uma expressão classe base. O motivo é certamente que o mecanismo de (des) alocação pode tentar inferir o tamanho do pedaço de memória para liberar do deleteoperando, por exemplo, quando existem várias arenas de alocação para objetos de determinados tamanhos. Essa restrição não se aplica à destruição normal de objetos com duração de armazenamento estático ou automático.
Peter - Restabelece Monica
@DavisHerring Acho que concordamos lá :-).
Peter - Restabelece Monica
@DavisHerring Ah, entendo, você se refere ao meu primeiro comentário - houve um IIUC nesse comentário e ele terminou em uma pergunta; Vi mais tarde que, de fato, é sempre proibido. (Basilevs fez uma declaração geral, "efetivamente impede", e fiquei pensando sobre a maneira específica como isso impede.) Então, sim, concordamos: UB.
Peter - Restabelece Monica
@Basilevs Isso deve ter sido inadvertido. Fixo.
ThomasMcLeod
36

Se você está considerando isso, claramente já matou os pedantes de idiomas em seu escritório. Com eles fora do caminho, por que não fazer

struct MyVector
{
   std::vector<Thingy> v;  // public!
   void func1( ... ) ; // and so on
}

Isso contornará todos os erros possíveis que podem resultar do aumento acidental de sua classe MyVector, e você ainda pode acessar todas as operações de vetor apenas adicionando um pouco .v.

Crashworks
fonte
E expor contêineres e algoritmos? Veja a resposta de Kos acima.
Bruno Nery
19

O que você espera conseguir? Apenas fornecendo alguma funcionalidade?

A maneira idiomática do C ++ para fazer isso é escrever apenas algumas funções gratuitas que implementam a funcionalidade. Provavelmente, você realmente não precisa de um std :: vector, especificamente para a funcionalidade que está implementando, o que significa que você está realmente perdendo a capacidade de reutilização tentando herdar do std :: vector.

Eu recomendo fortemente que você analise a biblioteca e os cabeçalhos padrão e medite sobre como eles funcionam.

Karl Knechtel
fonte
5
Não estou convencido. Você poderia atualizar com parte do código proposto para explicar o porquê?
Karl Knechtel
6
@ Armmen: além da estética, existem boas razões?
Snemarch 04/12/2010
12
@ Armmen: Melhor estética e maior genérica, seria fornecer gratuitamente fronte backtambém funções. :) (Considere também o exemplo de free begine endem C ++ 0x e boost.)
UncleBens 4/10/10
3
Ainda não sei o que há de errado com funções gratuitas. Se você não gosta da "estética" do STL, talvez o C ++ seja o lugar errado para você, esteticamente. E adicionar algumas funções de membro não o corrigirá, pois muitos outros algoritmos ainda são funções livres.
precisa
17
É difícil armazenar em cache um resultado de operação pesada em um algoritmo externo. Suponha que você precise calcular uma soma de todos os elementos no vetor ou resolver uma equação polinomial com elementos vetoriais como coeficientes. Essas operações são pesadas e a preguiça seria útil para elas. Mas você não pode apresentá-lo sem quebrar ou herdar do contêiner.
Basilevs
14

Penso que poucas regras devem ser seguidas às cegas 100% do tempo. Parece que você pensou bastante e está convencido de que esse é o caminho a seguir. Portanto, a menos que alguém tenha boas razões específicas para não fazer isso, acho que você deve prosseguir com seu plano.

NPE
fonte
9
Sua primeira frase é verdadeira 100% do tempo. :)
Steve Fallows
5
Infelizmente, a segunda frase não é. Ele não pensou muito nisso. A maior parte da pergunta é irrelevante. A única parte disso que mostra sua motivação é "Eu quero que eles sejam membros diretamente do vetor". Eu quero. Não há razão para isso ser desejável. Parece que ele não pensou em nada .
jalf
7

Não há razão para herdar, a std::vectormenos que se queira criar uma classe que funcione de maneira diferente std::vector, porque ela lida com os detalhes ocultos da std::vectordefinição de uma maneira, ou a menos que tenha razões ideológicas para usar os objetos dessa classe no lugar de std::vectorsão os No entanto, os criadores do padrão em C ++ não forneceram std::vectornenhuma interface (na forma de membros protegidos) que essa classe herdada pudesse tirar proveito para melhorar o vetor de uma maneira específica. Na verdade, eles não tinham como pensar em nenhum aspecto específico que pudesse precisar de extensão ou ajustar a implementação adicional, portanto, não precisavam pensar em fornecer nenhuma interface desse tipo para nenhum propósito.

As razões para a segunda opção podem ser apenas ideológicas, porque std::vectors não são polimórficas e, caso contrário, não há diferença se você expõe std::vectora interface pública da via herança pública ou associação pública. (Suponha que você precise manter algum estado em seu objeto para não se safar das funções livres). Em uma nota menos sólida e do ponto de vista ideológico, parece que std::vectors são uma espécie de "idéia simples"; portanto, qualquer complexidade na forma de objetos de diferentes classes possíveis em seu lugar ideologicamente não faz sentido.

Evgeniy
fonte
Ótima resposta. Bem-vindo ao SO!
Armen Tsirunyan
4

Em termos práticos: Se você não possui nenhum membro de dados em sua classe derivada, não possui problemas, nem mesmo no uso polimórfico. Você só precisa de um destruidor virtual se os tamanhos da classe base e da classe derivada forem diferentes e / ou você tiver funções virtuais (o que significa uma tabela em V).

MAS, em teoria: De [expr.delete] no C ++ 0x FCD: Na primeira alternativa (excluir objeto), se o tipo estático do objeto a ser excluído for diferente do seu tipo dinâmico, o tipo estático deve ser um classe base do tipo dinâmico do objeto a ser excluído e o tipo estático deve ter um destruidor virtual ou o comportamento é indefinido.

Mas você pode derivar privadamente do std :: vector sem problemas. Eu usei o seguinte padrão:

class PointVector : private std::vector<PointType>
{
    typedef std::vector<PointType> Vector;
    ...
    using Vector::at;
    using Vector::clear;
    using Vector::iterator;
    using Vector::const_iterator;
    using Vector::begin;
    using Vector::end;
    using Vector::cbegin;
    using Vector::cend;
    using Vector::crbegin;
    using Vector::crend;
    using Vector::empty;
    using Vector::size;
    using Vector::reserve;
    using Vector::operator[];
    using Vector::assign;
    using Vector::insert;
    using Vector::erase;
    using Vector::front;
    using Vector::back;
    using Vector::push_back;
    using Vector::pop_back;
    using Vector::resize;
    ...
hmuelner
fonte
3
"Você só precisa de um destruidor virtual se os tamanhos da classe base e da classe derivada forem diferentes e / ou você tiver funções virtuais (o que significa uma tabela em V)." Esta afirmação é praticamente correto, mas não teoricamente
Armen Tsirunyan
2
Sim, em princípio, ainda é um comportamento indefinido.
jalf
Se você afirma que esse é um comportamento indefinido, eu gostaria de ver uma prova (cotação da norma).
Hmuelner #
8
@hmuelner: Infelizmente, Armen e jalf estão corretos neste. De [expr.delete]no C ++ 0x FCD: <quote> Na primeira alternativa (excluir objeto), se o tipo estático do objeto a ser excluído for diferente de seu tipo dinâmico, o tipo estático deve ser uma classe base do tipo dinâmico do objeto a ser excluído e o tipo estático deve ter um destruidor virtual ou o comportamento é indefinido. </quote>
Ben Voigt
1
O que é engraçado, porque eu realmente pensei que o comportamento dependia da presença de um destruidor não trivial (especificamente, que as classes POD poderiam ser destruídas através de um ponteiro para a base).
Ben Voigt
3

Se você seguir um bom estilo C ++, a ausência de função virtual não é o problema, mas o fatiamento (consulte https://stackoverflow.com/a/14461532/877329 )

Por que a ausência de funções virtuais não é o problema? Porque uma função não deve tentar deletequalquer ponteiro que recebe, uma vez que não a possui. Portanto, se seguir políticas rígidas de propriedade, não serão necessários destruidores virtuais. Por exemplo, isso está sempre errado (com ou sem destruidor virtual):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    delete obj;
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj); //Will crash here. But caller does not know that
//  ...
    }

Por outro lado, isso sempre funcionará (com ou sem destruidor virtual):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj);
//  The correct destructor *will* be called here.
    }

Se o objeto for criado por uma fábrica, a fábrica também deve retornar um ponteiro para um deleter de trabalho, que deve ser usado em vez de delete, pois a fábrica pode usar seu próprio heap. O chamador pode obter a forma de um share_ptrou unique_ptr. Em suma, não deletequalquer coisa que você não obter directamente a partir new.

user877329
fonte
2

Sim, é seguro desde que você tome cuidado para não fazer coisas que não são seguras ... Acho que nunca vi alguém usar um vetor com um novo, então na prática você provavelmente ficará bem. No entanto, não é o idioma comum em c ++ ....

Você é capaz de fornecer mais informações sobre o que são os algoritmos?

Às vezes, você acaba seguindo um caminho com um design e, em seguida, não consegue ver os outros caminhos que você pode ter seguido - o fato de que você afirma ter que vetorizar com 10 novos algoritmos, toca campainhas de alarme para mim - existem realmente 10 fins gerais algoritmos que um vetor pode implementar ou você está tentando criar um objeto que seja um vetor de uso geral E que contenha funções específicas do aplicativo?

Certamente não estou dizendo que você não deve fazer isso, é apenas que, com as informações que você deu, os alarmes estão tocando, o que me faz pensar que talvez algo esteja errado com suas abstrações e que haja uma maneira melhor de conseguir o que você deseja. quer.

jcoder
fonte
2

Também herdei de std::vectorrecentemente e achei muito útil e até agora não tive problemas com isso.

Minha classe é uma classe de matriz esparsa, o que significa que eu preciso armazenar meus elementos da matriz em algum lugar, ou seja, em um std::vector. Minha razão para herdar foi que eu estava com preguiça de escrever interfaces para todos os métodos e também estou fazendo interface com a classe para Python via SWIG, onde já existe um bom código de interface std::vector. Achei muito mais fácil estender esse código de interface para minha classe, em vez de escrever um novo a partir do zero.

O único problema que eu posso ver com a abordagem não é tanto com o destrutor não-virtual, mas sim alguns outros métodos, o que eu gostaria de sobrecarga, tais como push_back(), resize(), insert()etc. herança Privada poderia realmente ser uma boa opção.

Obrigado!

Joel Andersson
fonte
10
Na minha experiência, o pior dano a longo prazo geralmente é causado por pessoas que tentam algo desaconselhado, e " até agora não experimentaram (leia-se notado ) nenhum problema com ele".
Disillusioned
0

Aqui, deixe-me apresentar mais duas maneiras de fazer o que você deseja. Uma é outra maneira de quebrar std::vector, outra é a maneira de herdar sem dar aos usuários a chance de quebrar qualquer coisa:

  1. Deixe-me adicionar outra maneira de quebrar std::vectorsem escrever muitos wrappers de função.

#include <utility> // For std:: forward
struct Derived: protected std::vector<T> {
    // Anything...
    using underlying_t = std::vector<T>;

    auto* get_underlying() noexcept
    {
        return static_cast<underlying_t*>(this);
    }
    auto* get_underlying() const noexcept
    {
        return static_cast<underlying_t*>(this);
    }

    template <class Ret, class ...Args>
    auto apply_to_underlying_class(Ret (*underlying_t::member_f)(Args...), Args &&...args)
    {
        return (get_underlying()->*member_f)(std::forward<Args>(args)...);
    }
};
  1. Herdar de std :: span em vez de std::vectore evitar o problema do dtor.
JiaHao Xu
fonte
0

É garantido que essa pergunta produz embreagem de pérola sem fôlego, mas, de fato, não há razão defensável para evitar, ou "multiplicar desnecessariamente entidades" para evitar derivação de um contêiner Standard. A expressão mais simples e mais curta possível é a mais clara e a melhor.

Você precisa exercer todo o cuidado usual em relação a qualquer tipo derivado, mas não há nada de especial no caso de uma base da Norma. Substituir uma função de membro base pode ser complicado, mas seria imprudente fazer com qualquer base não virtual, portanto, não há muito especial aqui. Se você adicionasse um membro de dados, precisaria se preocupar em fatiar se o membro tivesse que ser consistente com o conteúdo da base, mas novamente é o mesmo para qualquer base.

O local em que achei derivar de um contêiner padrão particularmente útil é adicionar um único construtor que faça exatamente a inicialização necessária, sem chance de confusão ou seqüestro por outros construtores. (Estou olhando para você, construtores initialization_list!) Então, você pode usar livremente o objeto resultante, fatiado - passe-o por referência a algo que espera a base, mova-o para uma instância da base, o que você tem. Não há casos extremos com os quais se preocupar, a menos que isso o incomode vincular um argumento de modelo à classe derivada.

Um local onde essa técnica será imediatamente útil no C ++ 20 é a reserva. Onde poderíamos ter escrito

  std::vector<T> names; names.reserve(1000);

nós podemos dizer

  template<typename C> 
  struct reserve_in : C { 
    reserve_in(std::size_t n) { this->reserve(n); }
  };

e depois, mesmo como alunos,

  . . .
  reserve_in<std::vector<T>> taken_names{1000};  // 1
  std::vector<T> given_names{reserve_in<std::vector<T>>{1000}}; // 2
  . . .

(de acordo com a preferência) e não precisa escrever um construtor apenas para chamar reserva () neles.

(A razão pela qual reserve_in , tecnicamente, precisa aguardar o C ++ 20 é que os Padrões anteriores não exigem que a capacidade de um vetor vazio seja preservada entre as jogadas. Isso é reconhecido como um descuido e pode-se esperar uma correção razoável como um defeito a tempo para o ano 20. Também podemos esperar que a correção seja, efetivamente, datada dos Padrões anteriores, porque todas as implementações existentes preservam a capacidade em todos os movimentos; os Padrões simplesmente não o exigiram. a reserva de armas é quase sempre apenas uma otimização.)

Alguns argumentam que o caso de reserve_iné melhor servido por um modelo de função livre:

  template<typename C> 
  auto reserve_in(std::size_t n) { C c; c.reserve(n); return c; }

Essa alternativa é certamente viável - e pode até, às vezes, ser infinitesimalmente mais rápida, por causa do * RVO. Mas a escolha da derivação ou função livre deve ser feita por seus próprios méritos, e não por superstições infundadas (heh!) Sobre derivar de componentes Padrão. No exemplo de uso acima, apenas o segundo formulário funcionaria com a função livre; embora fora do contexto de classe, poderia ser escrito de forma um pouco mais concisa:

  auto given_names{reserve_in<std::vector<T>>(1000)}; // 2
Nathan Myers
fonte