Item C ++ Efetivo 23 Prefere funções não-membro não-membro para funções-membro

Embora intrigante com alguns fatos sobre o design de classs, especificamente se as funções devem ser membros ou não, examinei C ++ efetivo e localizei o Item 23, a saber, Preferir funções não-membro não-membro a funções-membro. Lendo isso em primeira mão com o exemplo do navegador da web fez algum sentido, no entanto, as funções de conveniência (chamadas de funções não-membros como esta no livro) nesse exemplo mudam o estado da class, não é?

  • Então, primeira pergunta, eles não deveriam ser membros?

  • Lendo um pouco mais, ele considera as funções STL e, na verdade, algumas funções que não são implementadas por algumas classs são implementadas em stl. Seguindo as idéias do livro, elas evoluem para algumas funções de conveniência que são incluídas em alguns namespaces razoáveis, como std::sort , std::copy from algorithm . Por exemplo, a class vector não possui uma function de sort e uma usa a function stl sort para que não seja um membro da class vector. Mas também é possível estender o mesmo raciocínio para algumas outras funções na class vetorial, como assign modo que também não possa ser implementada como membro, mas como uma function de conveniência. No entanto, isso também altera o estado interno do object como tipo no qual ele operava. Então, qual é a razão por trás desta questão sutil, mas importante (eu acho).

Se você tiver access ao livro, você pode esclarecer esses pontos um pouco mais para mim?

O access ao livro não é necessário.

As questões que estamos lidando aqui são Dependência e Reutilização .

Em um software bem projetado, você tenta isolar os itens uns dos outros para reduzir as Dependências, porque as Dependências são um obstáculo a ser superado quando a mudança é necessária.

Em um software bem projetado, você aplica o princípio DRY (Don’t Repeat Yourself), porque quando uma mudança é necessária, é doloroso e propenso a erros ter que repeti-la em uma dúzia de lugares diferentes.

A mentalidade OO “clássica” é cada vez mais ruim em lidar com dependencies. Por ter muitos e muitos methods dependendo diretamente dos componentes internos da class, a menor mudança implica uma reescrita completa. Não precisa ser assim.

Em C ++, o STL (não toda a biblioteca padrão) foi projetado com os objectives explícitos de:

  • dependencies de corte
  • permitindo a reutilização

Portanto, os Containers expõem interfaces bem definidas que ocultam suas representações internas, mas ainda oferecem access suficiente às informações que eles encapsulam para que os Algoritmos possam ser executados neles. Todas as modificações são feitas através da interface do contêiner para que as invariantes sejam garantidas.

Por exemplo, se você pensar nos requisitos do algoritmo de sort . Para a implementação usada (em geral) pelo STL, requer (do contêiner):

  • access eficiente a um item em um determinado índice: Acesso Aleatório
  • a capacidade de trocar dois itens: não associativo

Assim, qualquer contêiner que forneça access random e não seja associativo é (em teoria) adequado para ser classificado de forma eficiente por (digamos) um algoritmo de sorting rápida.

Quais são os Containers em C ++ que satisfazem isso?

  • o array C básico
  • deque
  • vector

E qualquer recipiente que você possa escrever se prestar atenção a esses detalhes.

Seria um desperdício, não seria, rewrite (copiar / colar / ajustar) para cada um desses?

Note, por exemplo, que existe um método std::list::sort . Por quê ? Porque std::list não oferece access random (informalmente myList[4] não funciona), portanto, o sort de algoritmo não é adequado.

O critério que uso é se uma function pode ser implementada de forma significativamente mais eficiente por ser uma function de membro, então deve ser uma function de membro. ::std::sort não atende a essa definição. De fato, não há diferença de eficiência em implementá-lo externamente versus internamente.

Uma grande melhoria de eficiência implementando algo como uma function de membro (ou amigo) significa que é muito benéfico conhecer o estado interno da class.

Parte da arte do design de interface é a arte de encontrar o conjunto mínimo de funções-membro, de modo que todas as operações que você queira executar no object possam ser implementadas de maneira razoavelmente eficiente em termos delas. E esse conjunto não deve suportar operações que não devem ser executadas na class. Então você não pode simplesmente implementar um monte de funções getter e setter e chamá-lo de bom.

Eu acho que a razão para essa regra é que usando funções de membro você pode confiar demais nos internos de uma class por acidente. Alterar o estado de uma class não é um problema. O problema real é a quantidade de código que você precisa mudar se modificar alguma propriedade privada dentro de sua class. Manter a interface da class (methods públicos) o menor possível reduz tanto a quantidade de trabalho que você precisará fazer nesse caso quanto o risco de fazer algo estranho com seus dados privados, deixando-o com uma instância em um estado inconsistente .

AtoMerZ também está certo, as funções não-membro não-membro podem ser modeladas e reutilizadas para outros tipos também.

A propósito, você deve comprar sua cópia do Effective C ++, é um ótimo livro, mas não tente sempre cumprir com todos os itens deste livro. Orientado a Objetos Design tanto de boas práticas (de livros, etc.) como de experiência (acho que também está escrito em Effective C ++ em algum lugar).

Então, primeira pergunta, eles não deveriam ser membros?

Não, isso não segue. No design de class C ++ idiomática (pelo menos, nos idiomas usados ​​no Effective C ++ ), as funções não-membro não-membro estendem a interface de class. Eles podem ser considerados parte da API pública para a class, apesar do fato de não precisarem e não terem access privado à class. Se este design é “não OOP” por alguma definição de OOP então, OK, o C ++ idiomático não é OOP por essa definição.

estender o mesmo raciocínio para algumas outras funções na class de vetores

Isso é verdade, existem algumas funções-membro de contêineres padrão que poderiam ter sido funções livres. Por exemplo, vector::push_back é definido em termos de insert , e certamente poderia ser implementado sem access privado à class. Nesse caso, no entanto, push_back faz parte de um conceito abstrato, o BackInsertionSequence , implementado por esse vetor. Esses conceitos genéricos atravessam o design de classs específicas, portanto, se você está projetando ou implementando seus próprios conceitos genéricos que podem influenciar onde você coloca funções.

Certamente há partes do padrão que provavelmente deveriam ter sido diferentes, por exemplo, std :: string tem muitas funções de membro . Mas o que está feito está feito, e essas classs foram projetadas antes que as pessoas realmente se estabelecessem no que agora podemos chamar de estilo moderno de C ++. A aula funciona de qualquer forma, então só há muito benefício prático que você pode ter de se preocupar com a diferença.

A motivação é simples: mantenha uma syntax consistente. À medida que a class evolui ou é usada, várias funções de conveniência que não são membros aparecerão; você não deseja modificar a interface de class para adicionar algo como toUpper a uma class de string, por exemplo. (No caso de std::string , é claro, você não pode.) A preocupação de Scott é que quando isso acontece, você acaba com uma syntax inconsistente:

 s.insert( "abc" ); toUpper( s ); 

Usando apenas funções livres, declarando-as como amigo, todas as funções têm a mesma syntax. A alternativa seria modificar a definição de class toda vez que você adicionar uma function de conveniência.

Eu não estou totalmente convencido. Se uma class é bem projetada, tem uma funcionalidade básica, é claro para o usuário quais funções fazem parte dessa funcionalidade básica e quais são funções adicionais de conveniência (se houver alguma). Globalmente, string é uma espécie de caso especial, porque foi projetado para ser usado para resolver muitos problemas diferentes; Eu não posso imaginar este sendo o caso de muitas classs.

Vários pensamentos:

  • É legal quando os não membros trabalham com a API pública da class, pois reduz a quantidade de código que:
    • precisa ser cuidadosamente monitorado para garantir invariantes de class,
    • precisa ser alterado se a implementação do object for reprojetada.
  • Quando isso não é bom o suficiente, um não membro ainda pode ser feito um friend .
  • Escrever uma function não-membro é geralmente um pouco menos conveniente, já que os membros não estão implicitamente no escopo, MAS se você considerar a evolução do programa:
    • Uma vez que existe uma function não membro e se perceba que a mesma funcionalidade seria útil para outros tipos, geralmente é muito fácil converter a function em um modelo e disponibilizá-la não apenas para os dois tipos, mas também para tipos futuros arbitrários. Em outras palavras, os modelos de não membros permitem uma reutilização de algoritmos ainda mais flexível do que o polymorphism de tempo de execução / despacho virtual: os modelos permitem algo conhecido como tipagem de pato .
    • Um tipo existente que ostenta uma function de membro útil incentiva o recorte e colagem para outros tipos que gostariam de um comportamento análogo porque a maioria das maneiras de converter a function para reutilização requer que todo access de membro implícito seja feito um access explícito em um object específico, que vai ser mais um tedius 30 + segundos para o programador ….
  • Funções membro permitem a object.function(x, y, z) , que IMHO é muito conveniente, expressivo e intuitivo. Eles também funcionam melhor com resources de descoberta / conclusão em muitos IDEs.
  • Uma separação como funções de membro e de não membro pode ajudar a comunicar a natureza essencial da class, é invariantes e operações fundamentais e agrupa logicamente os resources de “conveniência” add-on e possivelmente ad-hoc. Considere a sabedoria de Tony Hoare:

    “Há duas maneiras de construir um design de software: uma maneira é torná-lo tão simples que obviamente não há deficiências, e a outra maneira é torná-lo tão complicado que não há deficiências óbvias. O primeiro método é muito mais difícil ”

    • Aqui, o uso de não-membros não é necessariamente muito mais difícil, mas você precisa pensar mais sobre como está acessando dados de membros, methods privados / protegidos, por que e quais operações são fundamentais. Tal busca da alma melhoraria o design com funções de membro também, é mais fácil ser preguiçoso sobre: ​​- /.
  • À medida que a funcionalidade de não membro se expande em sofisticação ou capta dependencies adicionais, as funções podem ser movidas para headers e arquivos de implementação separados, até mesmo bibliotecas, para que os usuários da funcionalidade central apenas “paguem” pelo uso das partes desejadas.

(A resposta do Omnifarious é uma leitura obrigatória, três vezes se for novidade para você.)

Acho que o tipo não é implementado como uma function-membro porque é amplamente utilizado, não apenas para vetores. Se eles tivessem isso como uma function de membro, eles teriam que reimplementá-lo a cada vez para cada contêiner usando-o. Então eu acho que é para facilitar a implementação.