Acabei de ler um dos artigos de Joel em que ele diz:
Em geral, tenho que admitir que tenho um pouco de medo de recursos de linguagem que ocultam as coisas . Quando você vê o código
i = j * 5;
... em C, você sabe, pelo menos, que j está sendo multiplicado por cinco e os resultados armazenados em i.
Mas se você vir esse mesmo trecho de código em C ++, não saberá nada. Nada. A única maneira de saber o que realmente está acontecendo no C ++ é descobrir quais são os tipos iej, algo que pode ser declarado em outro lugar. Isso porque j pode ser de um tipo que tenha
operator*
sobrecarregado e faz algo terrivelmente espirituoso quando você tenta multiplicá-lo.
(Ênfase minha.) Com medo de recursos de linguagem que ocultam as coisas? Como você pode ter medo disso? Ocultar coisas (também conhecidas como abstração ) não é uma das idéias principais da programação orientada a objetos? Sempre que você chama um método a.foo(b)
, você não tem idéia do que isso pode fazer. Você precisa descobrir quais tipos a
e quais b
são, algo que pode ser declarado em algum outro lugar. Então, devemos acabar com a programação orientada a objetos, porque esconde muitas coisas do programador?
E como é j * 5
diferente de j.multiply(5)
qual você pode escrever em um idioma que não suporta sobrecarga de operador? Novamente, você teria que descobrir o tipo j
e espiar dentro do multiply
método, porque eis que ele j
pode ser de um tipo que possui um multiply
método que faz algo terrivelmente espirituoso.
"Muahaha, sou um programador malvado que nomeia um método multiply
, mas o que ele realmente faz é totalmente obscuro e não intuitivo e não tem absolutamente nada a ver com a multiplicação das coisas". Esse é um cenário que devemos levar em consideração ao projetar uma linguagem de programação? Então, temos que abandonar identificadores de linguagens de programação com o argumento de que eles podem ser enganosos!
Se você quiser saber o que um método faz, pode dar uma olhada na documentação ou espiar dentro da implementação. Sobrecarga de operador é apenas açúcar sintático, e não vejo como isso muda o jogo.
Por favor me esclareça.
Respostas:
A abstração 'oculta' o código para que você não precise se preocupar com o funcionamento interno e, muitas vezes, para não poder alterá-lo, mas a intenção não era impedir que você o visse. Apenas fazemos suposições sobre os operadores e, como Joel disse, pode estar em qualquer lugar. Ter um recurso de programação que exija que todos os operadores sobrecarregados sejam estabelecidos em um local específico pode ajudar a encontrá-lo, mas não tenho certeza se isso facilita o uso.
Não vejo fazer * fazer algo que não se pareça com a multiplicação melhor do que uma função chamada Get_Some_Data que exclui dados.
fonte
<<
operador definido em fluxos que nada tem a ver com a mudança bit a bit, diretamente na biblioteca padrão do C ++.IMHO, recursos de linguagem como sobrecarga do operador dão ao programador mais poder. E, como todos sabemos, com grande poder vem uma grande responsabilidade. Os recursos que lhe dão mais poder também oferecem mais maneiras de dar um tiro no próprio pé e, obviamente, devem ser usados criteriosamente.
Por exemplo, faz todo o sentido sobrecarregar
+
o*
operador ou o operadorclass Matrix
orclass Complex
. Todos saberão instantaneamente o que isso significa. Por outro lado, para mim, o fato de+
significar concatenação de strings não é de todo óbvio, mesmo que Java faça isso como parte da linguagem e STL faça porstd::string
usar sobrecarga de operador.Outro bom exemplo de quando a sobrecarga do operador torna o código mais claro são os ponteiros inteligentes em C ++. Você deseja que os ponteiros inteligentes se comportem como ponteiros regulares o máximo possível, por isso faz todo o sentido sobrecarregar os unários
*
e os->
operadores.Em essência, a sobrecarga do operador nada mais é do que outra maneira de nomear uma função. E existe uma regra para nomear funções: o nome deve ser descritivo, tornando imediatamente óbvio o que a função faz. A mesma regra exata se aplica à sobrecarga do operador.
fonte
*
-los pode causar confusão. Pode-se argumentar que você poderia usaroperator*()
o produto escalar eoperator%()
o produto cruzado, mas eu não faria isso em uma biblioteca de uso geral.A-B
comoB-A
seja, e todos os operadores seguir aquele padrão. Embora sempre exista uma exceção: quando o compilador pode provar que não importa, é permitido reorganizar tudo.No Haskell "+", "-", "*", "/" etc são apenas funções (infix).
Você deve nomear uma função infix "mais" como em "4 mais 2"? Por que não, se adição é o que sua função faz. Você deve nomear sua função "mais" como "+"? Por que não.
Penso que o problema com os chamados "operadores" é que eles se assemelham principalmente a operações matemáticas e não existem muitas maneiras de interpretá-las e, portanto, há grandes expectativas sobre o que esse método / função / operador faz.
EDIT: deixou meu argumento mais claro
fonte
int
,float
,long long
e qualquer que seja. Então, o que é isso tudo?+
para diferentes tipos de números internos, mas à criação de sobrecargas definidas pelo usuário. daqui meu comentário.Com base nas outras respostas que já vi, só posso concluir que a verdadeira objeção à sobrecarga do operador é o desejo de código imediatamente óbvio.
Isso é trágico por dois motivos:
fonte
Eu concordo um pouco.
Se você escrever
multiply(j,5)
,j
pode ser do tipo escalar ou matriz, tornando-semultiply()
mais ou menos complexo, dependendo do quej
é. No entanto, se você abandonar completamente a idéia de sobrecarregar, a função terá que ser nomeadamultiply_scalar()
ou omultiply_matrix()
que tornaria óbvio o que está acontecendo por baixo.Há código em que muitos de nós preferiríamos de uma maneira e há código em que a maioria de nós preferia de outra maneira. A maior parte do código, no entanto, cai no meio termo entre esses dois extremos. O que você prefere depende do seu histórico e preferências pessoais.
fonte
multiply_matrix()
também não gostam de programação genérica.real_multiply()
ou menos. Os desenvolvedores geralmente não são bons com nomes eoperator*()
pelo menos serão consistentes.operator*()
possa fazer algo estúpido,j
é uma macro avaliada para expressões que envolvem cinco chamadas de função e outras coisas. Então você não pode mais comparar as duas abordagens. Mas, sim, nomear bem as coisas é difícil, embora valha a pena o tempo que for necessário.Eu vejo dois problemas com sobrecarga do operador.
&&
,||
ou,
, você perde os pontos de seqüência que estão implícitas pelas variantes embutidos destes operadores (bem como o comportamento de curto-circuito dos operadores lógicos). Por esse motivo, é melhor não sobrecarregar esses operadores, mesmo que o idioma permita.operator<<
para fluxos.fonte
&&
e||
de uma maneira que não implique seqüenciamento foi um grande erro (IMHO, se o C ++ permitiria a sobrecarga desses, ele deveria ter usado um formato especial de "duas funções", com a primeira função sendo necessária para retornar um tipo implicitamente conversível em um número inteiro; a segunda função pode receber dois ou três argumentos, com o argumento "extra" da segunda função sendo o tipo de retorno do primeiro. O compilador chamaria a primeira função e, em seguida, se ele voltou diferente de zero, avaliar o segundo operando e chamar a segunda função em cima dele).foo.bar[3].X
fosse tratada pelafoo
classe de, em vez de exigirfoo
expor um membro que pode oferecer suporte à assinatura e expor um membroX
. Se alguém quisesse forçar a avaliação via acesso real dos membros, escreveria((foo.bar)[3]).X
.Com base na minha experiência pessoal, a maneira Java de permitir vários métodos, mas não sobrecarregar o operador, significa que sempre que você vê um operador, sabe exatamente o que ele faz.
Você não precisa ver se
*
chama um código estranho, mas sabe que é uma multiplicação e se comporta exatamente como na maneira definida pela Especificação de Linguagem Java. Isso significa que você pode se concentrar no comportamento real em vez de descobrir todas as coisas definidas pelo programador.Em outras palavras, proibir a sobrecarga do operador é um benefício para o leitor , não para o gravador e, portanto, facilita a manutenção dos programas!
fonte
list.get(n)
sintaxe?std::list
não sobrecarregaoperator[]
(ou fornece qualquer outro meio de indexação na lista), porque essa operação seria O (n), e uma interface de lista não deve expor essa função se você se preocupa com a eficiência. Os clientes podem ficar tentados a repetir listas vinculadas com índices, tornando os algoritmos O (n) desnecessariamente O (n ^ 2). Você vê isso frequentemente no código Java, especialmente se as pessoas trabalham com aList
interface que visa abstrair completamente a complexidade.time.add(anotherTime)
vir, também precisará verificar se o programador da biblioteca implementou a operação de adição "corretamente" (o que isso significa).Uma diferença entre sobrecarga
a * b
e chamadamultiply(a,b)
é que a última pode ser facilmente recebida. Se amultiply
função não estiver sobrecarregada para tipos diferentes, você poderá descobrir exatamente o que a função fará, sem precisar rastrear os tipos dea
eb
.Linus Torvalds tem um argumento interessante sobre a sobrecarga do operador. Em algo como o desenvolvimento do kernel Linux, onde a maioria das alterações é enviada por meio de correções por email, é importante que os mantenedores possam entender o que um patch fará com apenas algumas linhas de contexto em torno de cada alteração. Se funções e operadores não estiverem sobrecarregados, o patch poderá ser lido com mais facilidade de maneira independente do contexto, pois você não precisará examinar o arquivo alterado para descobrir quais são todos os tipos e verificar se há operadores sobrecarregados.
fonte
Suspeito que tenha algo a ver com quebrar expectativas. Já estou acostumado a C ++, o comportamento do operador não é totalmente determinado pela linguagem e você não ficará surpreso quando um operador fizer algo estranho. Se você está acostumado a idiomas que não possuem esse recurso e vê o código C ++, traz consigo as expectativas desses outros idiomas e pode se surpreender ao descobrir que um operador sobrecarregado faz algo estranho.
Pessoalmente, acho que há uma diferença. Quando você pode alterar o comportamento da sintaxe interna do idioma, torna-se mais opaco para se pensar. Os idiomas que não permitem a metaprogramação são sintaticamente menos poderosos, mas conceitualmente mais simples de entender.
fonte
Eu acho que sobrecarregar operadores matemáticos não é o problema real da sobrecarga de operadores em C ++. Eu acho que sobrecarregar operadores que não devem confiar no contexto da expressão (ou seja, tipo) é "mau". Por exemplo, sobrecarga
,
[ ]
( )
->
->*
new
delete
ou mesmo o unário*
. Você tem um certo conjunto de expectativas daqueles operadores que nunca devem mudar.fonte
operator[]
, functors semoperator()
, ponteiros inteligentes semoperator->
e assim por diante.[]
deve sempre ser um acessador do tipo matriz e->
deve sempre significar acessar um membro. Não importa se é realmente uma matriz ou um contêiner diferente, ou se é um ponteiro inteligente ou não.Entendo perfeitamente que você não gosta do argumento de Joel sobre se esconder. Nem eu. É realmente muito melhor usar '+' para coisas como tipos numéricos incorporados ou para os seus próprios, como, por exemplo, matriz. Admito que isso é puro e elegante para poder multiplicar duas matrizes com o '*' em vez de '.multiply ()'. E, afinal, temos o mesmo tipo de abstração nos dois casos.
O que dói aqui é a legibilidade do seu código. Na vida real, não no exemplo acadêmico de multiplicação de matrizes. Especialmente se a sua linguagem permitir definir operadores que não estão presentes inicialmente no núcleo da linguagem, por exemplo
=:=
. Muitas perguntas extras surgem neste momento. O que é esse maldito operador? Quero dizer, qual é a precedência dessa coisa? Qual é a associatividade? Em que ordem éa =:= b =:= c
realmente executada?Esse já é um argumento contra a sobrecarga do operador. Ainda não está convencido? Verificar as regras de precedência levou mais de 10 segundos? Ok, vamos mais longe.
Se você começar a usar uma linguagem que permita a sobrecarga do operador, por exemplo, a popular cujo nome começa com 'S', você aprenderá rapidamente que os designers de bibliotecas gostam de substituir os operadores. É claro que eles são bem-educados, seguem as melhores práticas (sem cinismo aqui) e todas as suas APIs fazem todo sentido quando as olhamos separadamente.
Agora imagine que você precisa usar algumas APIs que fazem uso pesado de operadores que sobrecarregam juntos em um único pedaço de código. Ou melhor ainda - você precisa ler um código legado como esse. É quando a sobrecarga do operador é realmente péssima. Basicamente, se houver muitos operadores sobrecarregados em um local, eles logo começarão a se misturar com os outros caracteres não alfanuméricos no código do programa. Eles se misturam com caracteres não alfanuméricos que não são realmente operadores, mas com alguns elementos gramaticais de linguagem mais fundamentais que definem coisas como blocos e escopos, modelam instruções de controle de fluxo ou denotam algumas meta-coisas. Você precisará colocar os óculos e aproximar os olhos 10 cm da tela LCD para entender essa bagunça visual.
fonte
Em geral, evito usar a sobrecarga do operador de maneiras não intuitivas. Ou seja, se eu tiver uma classe numérica, a sobrecarga * é aceitável (e incentivada). No entanto, se eu tiver uma classe Employee, o que sobrecarregar * faria? Em outras palavras, sobrecarregue os operadores de maneiras intuitivas que facilitam a leitura e a compreensão.
Aceitável / Incentivado:
Não aceitável:
fonte
Além do que já foi dito aqui, há mais um argumento contra a sobrecarga do operador. De fato, se você escreve
+
, isso é óbvio que você quer dizer adição de algo a algo. Mas nem sempre é esse o caso.O próprio C ++ fornece um ótimo exemplo desse caso. Como
stream << 1
deve ser lido? fluxo mudou para a esquerda por 1? Não é óbvio, a menos que você saiba explicitamente que << em C ++ também grava no fluxo. No entanto, se essa operação fosse implementada como um método, nenhum desenvolvedor sadio escreveriao.leftShift(1)
, seria algo parecidoo.write(1)
.A conclusão é que, ao tornar a sobrecarga do operador indisponível, a linguagem faz com que os programadores pensem nos nomes das operações. Mesmo que o nome escolhido não seja perfeito, ainda é mais difícil interpretar um nome do que um sinal.
fonte
Em comparação aos métodos detalhados, os operadores são mais curtos, mas também não exigem parênteses. Parênteses são relativamente inconvenientes para digitar. E você deve equilibrá-los. No total, qualquer chamada de método requer três caracteres de ruído simples em comparação com um operador. Isso torna o uso de operadores muito, muito tentador.
Por que mais alguém iria querer isso
cout << "Hello world"
:?O problema com a sobrecarga é que a maioria dos programadores é incrivelmente preguiçosa e a maioria dos programadores não pode se dar ao luxo de ser.
O que leva os programadores de C ++ a abusar da sobrecarga do operador não é a sua presença, mas a ausência de uma maneira mais clara de executar chamadas de método. E as pessoas não têm apenas medo de sobrecarregar o operador porque isso é possível, mas porque está feito.
Observe que, por exemplo, em Ruby e Scala, ninguém tem medo de sobrecarregar o operador. Além do fato de que o uso de operadores não é realmente mais curto que os métodos, outro motivo é que Ruby limita a sobrecarga de operadores a um mínimo sensato, enquanto o Scala permite que você declare seus próprios operadores , evitando assim a trivialidade de colisões.
fonte
A razão pela qual a Sobrecarga do operador é assustadora, é porque há um grande número de programadores que nunca pensariam que
*
não significa simplesmente "multiplicar", enquanto um método comofoo.multiply(bar)
pelo menos instantaneamente indica ao programador que alguém escreveu um método de multiplicação personalizado . Nesse momento, eles se perguntariam o porquê e iriam investigar.Eu trabalhei com "bons programadores" que estavam em posições de alto nível que criariam métodos chamados "CompareValues" que levariam 2 argumentos e aplicariam os valores de um ao outro e retornariam um booleano. Ou um método chamado "LoadTheValues" que iria ao banco de dados para outros 3 objetos, obter valores, fazer cálculos, modificar
this
e salvá-lo no banco de dados.Se estou trabalhando em uma equipe com esses tipos de programadores, sei instantaneamente investigar as coisas em que eles trabalharam. Se eles sobrecarregaram um operador, não tenho como saber que eles fizeram isso, exceto supor que eles fizeram e continuar procurando.
Em um mundo perfeito, ou em uma equipe com programadores perfeitos, a sobrecarga do operador é provavelmente uma ferramenta fantástica. Ainda tenho que trabalhar em uma equipe de programadores perfeitos, por isso é assustador.
fonte