Ir ao conteúdo

Posts recomendados

Postado

Olá pessoal. Eu vi um exemplo simples de um código(vou colocar abaixo) que sobrecarrega o operador -> e não consegui entender a "mecânica" do funcionamento. Na verdade eu também não sei como este operador funciona sem estar sobrecarregado, eu só sei usar ele. Também não consegui entender qual é o propósito de se sobrecarregar este operador, visto que ele sobrecarregado só serve para substituir o operador ponto.

#include <iostream>

class Seta {
public:
	int i;
	Seta* operator->() { return this; }
};

int main() {
	Seta obj;

	obj->i = 10;
	std::cout << "Com ponto: " << obj.i << std::endl << "Com seta: " << obj->i;
}

 

  • Amei 1
Postado

Olá

 

Eu nunca entendi a razão desse nome, "sobrecarga" ou antes "overloading". Trata-se de uma redefinição apenas. 

 

Em 28/09/2019 às 15:15, Bimetal disse:

Na verdade eu também não sei como este operador funciona sem estar sobrecarregado, eu só sei usar ele. Também não consegui entender qual é o propósito de se sobrecarregar este operador, visto que ele sobrecarregado só serve para substituir o operador ponto

 

Não consegui entender o que quis dizer nesse parágrafo :( talvez você pudesse explicar com outras palavras 

 

De volta ao exemplo e ao operador em si

 

Sobre a declaração:  esse operador retorna um ponteiro para a classe e não aceita parâmetros. Na implementacão exemplo apenas retorna o próprio argumento implícito, this. Um comportamento similar ao do operador -> para um ponteiro. 

 

Nesse caso do exemplo ele vai deixar o uso de -> como alternativa ao operador .  exatamente como seria se estivesse acessando um ponteiro pra o objeto e não o próprio objeto. Veja esse exemplo --- um pouco mais instrutivo eu acho -- isso não quer dizer legível :D:

#include <iostream>
class Seta
{
public:
    int i;
    Seta* operator->() { return this; }
};

struct Coisa
{
    int membro;
};

int main()
{
    Seta    obj;
    Seta*    outroObj = &obj;
    Coisa    coisa;
    Coisa*    pCoisa = &coisa;

    obj->i = 10;
    std::cout << "obj.i : "         << obj.i << std::endl;
    std::cout << "obj->i : "        << obj->i << std::endl;
    std::cout << "outroObj->i "     << outroObj->i << std::endl;
    std::cout << "(*outroObj)->i "  << (*outroObj)->i << std::endl;

    coisa.membro = 12;
    pCoisa->membro = 11;
    /*
    coisa->membro = 1;    // expressao invalida
     */
}    // end main()

Todos os cout() vão imprimir o valor 10

 

Mas veja o que está em comentários: para a classe Coisa eu não posso escrever coisa->membro = 1 porque não vai compilar: coisa foi declarado como 

Coisa coisa;

Mas eu preciso escrever pCoisa->membro = 11 porque pCoisa foi declarado

Coisa* pCoisa;

Agora se eu redefinir o operador -> para essa classe como no exemplo eu poderia usar os dois indistintamente.

Acho esse um exemplo inútil e bem besta. Tem autores que parecem fascinados pela possibilidade de sobrecarregar operadores em C++ mas que não pensam muito ao criar exemplos. E outros autores parecem  furiosos porque isso segundo eles criaria programas difíceis de ler e manter.

 

Mas poder redefinir um operador permite que ao definir uma classe você possa definir o comportamento dos operadores para instâncias de sua classe e isso permite que você escreva essas funções de um modo que faça sentido.

Imagine uma classe trem em que você possa usar locomotivas e vagões. E você tenha locomotivas de várias potências e vagões de vários pesos e capacidades.

  • Cada instância de trem pode ter uma configuração variável dessas coisas. Então se eu puder redefinir os operadores aritméticos eu posso definir como somar ou subtrair trens por exemplo, e nos métodos se pode somar os pesos e número dos vagões. 
  • Eu posso definir como é somar uma instância de locomotiva ao trem e assim ter o comprimento do trem, potência, capacidades de frenagem e tudo atualizados na classe sem escrever mais uma linha de código. Eu posso definir como é adicionar um vagão ao trem e gerar uma exceção por overflow se exceder o comprimento permitido, em metros!
  • Eu posso definir que somar um trem ao outro seja somar as locomotivas e os vagões e ao listar a composição tudo está atualizado como mágica...

 

Por outro lado, os operadores podem ser apenas reaproveitados para qualquer uso... Uma classe deslocamento por exemplo poderia usar os operadores + - * e * para mover um modelo para norte, sul, leste e oeste.

 

Uma classe folclórica poderia usar + para subtrair e - para somar. Assim é. E é um grande recurso se considerar o que eu expliquei. Se eu tiver tempo nos próximos dias posto aqui um exemplo com classes trem, locomotiva e vagão e uns operadores exemplo

Postado

Oi @arfneto. Eu sei que, por exemplo: a->b é igual a (*a).b, mas eu não sei como isso funciona de forma mais interna. Como eu não sei como funciona o operador, eu não tenho como saber como funciona a sobrecarga. Eu disse também que eu não entendi o propósito da sobrecarga deste operador porque, de acordo com o exemplo que eu coloquei ai, ele simplesmente me permite fazer algo que eu já posso fazer com o operador ponto.

Postado

Olá @Bimetal

 

Eu acho mesmo que aquele é um exemplo besta. Mas eu já havia visto isso antes com outros nomes e em outra língua. E que naquele exemplo sem redefinir você não poderia usar tal operador. Entenda que ao redefinir -> permite que você use o operador em uma situação em que normalmente ele não funcionaria, como te mostrei. Mas só isso.

 

34 minutos atrás, Bimetal disse:

a->b é igual a (*a).b

 

Bem, isso vai sempre depender

  • de como 'a' foi declarado
  • do atual significado do operador -> para a classe 'a' que pode não ser um tipo padrão
37 minutos atrás, Bimetal disse:

Como eu não sei como funciona o operador, eu não tenho como saber como funciona a sobrecarga

 

Na verdade é o contrário: ao redefinir o operador você é que vai dizer como funciona. Leu a parte em que falei do exemplo de uma classe trem? Você redefine operadores para que eles funcionem com classes especializadas que você escreveu.

 

Dois casos clássicos:

 

Overload = == e !=

    Muitas vezes comparar ou copiar dois objetos de uma classe envolve procedimentos não triviais, como percorrer listas internas e comparar resultados de consultas dinâmicas, de modo que você precisa interceptar isso no seu programa e escrever sua própria rotina.

Overload <<

    Muitas vezes, em especial em programas de teste para classes que você está desenvolvendo ainda, você pode querer muito isso, para poder escrever algo assim

MuitoComplexa       m1;

std::cout << m1 << endl;

e ter uma saída adequada para o que você quer mostrar de sua classe no momento, porque é muito mais prático que ficar escrevendo comandos de um metro para cada vez que for preciso testar. E se aparece uma novidade você só precisa alterar na implementação do operador <<

 

Como eu disse, nada impede que você crie operador + que multiplica, operador [] que lista, tudo depende da necessidade e sentido no momento.

 

No geral você não faz isso a menos que seja obrigado. Por exemplo, quase sempre que você precisa de um construtor de cópia é porque sua classe por exemplo aloca memória e tem listas e ponteiros e tal. E aí não dá pra usar

ClasseComPonteirosEstrutrasComplexas    coisa;
ClasseComPonteirosEstrutrasComplexas    outraCoisa;
ClasseComPonteirosEstrutrasComplexas    mesmaCoisa = coisa;

coisa = outraCoisa = mesmaCoisa;

Porque vai certamente abortar seu programa. Então você calmamente

  • sobrecarrega os operadores = != == e talvez outros
  • cria um destrutor pra liberar a memória na ordem inversa da que alocou as coisas lá dentro
  • cria um construtor de cópia para alocar memória e recriar as estruturas de dados para criar uma nova cópia de sua instância da classe

Ou não vai funcionar.

 

 

Postado

Realmente aprender C++ pode vir a ser uma experiencia bastante confusa, estamos falando de uma linguagem com mais de 40 anos e de la pra ca muita coisa foi mudada, aperfeiçoada e implementada. Os posts do @arfneto foram mais esclarecedores que muita coisa que achei por ai na internet, difícil encontrar quem realmente entende a filosofia do C++, a maioria da uns exemplos como esse que você citou, o cara sabe a sintaxe de como fazer isso ou aquilo mas provavelmente não sabe colocar um exemplo útil de fato.

 

Pelo que entendi sobrecarregar operadores para criar novos tipos não é obrigatório mas é bem útil pra tornar código mais legível ou obter uma saída diferenciada. Somar objetos por ex para obter uma media pode ser interessante colocar a função  diretamente no + do que usar um método tradicional, vale lembrar que o "operator" nesse contexto também é uma função

Ai ao invés de querer entender o que o compilador faz por de traz da cortina, o que pode ser um tiro no pé para um iniciante é melhor ter em mente que você e o compilador sabem como funciona o "+" e como é usado.

 

resultado = 2 + 2; <- Isso aqui esta ok pro compilador e pra nos seres humanos

 

objeto3 = objeto1 + objeto2; <- ai complicou pro lado do compilador, ele não tem ideá de como isso funciona

 

Então você precisa instrui-lo a "interpretar" o tipo que você esta criando. Agora o que vai dizer quando usar e pra que usar vai depender da sua experiencia e conhecimento na linguagem.

Postado
7 horas atrás, Benjamin Breeg disse:

criar novos tipos não é obrigatório mas é bem útil pra tornar código mais legível ou obter uma saída diferenciada

 

Sim. Mas criar classes e todo o conceito que vem junto cria programas mais sólidos e simboliza melhor toda uma classe de problemas. E é preciso considerar as classes que estão disponíveis para você usar, coisas impressionantes como a biblioteca padrão, as classes .Net,  cloisas como o DirectX e o openGL...

 

7 horas atrás, Benjamin Breeg disse:

o que o compilador faz por de traz da cortina, o que pode ser um tiro no pé para um iniciante é melhor ter em mente que você e o compilador sabem como funciona o "+" e como é usado.

 

resultado = 2 + 2; <- Isso aqui esta ok pro compilador e pra nos seres humanos

 

objeto3 = objeto1 + objeto2; <- ai complicou pro lado do compilador, ele não tem ideá de como isso funciona

 

Então você precisa instrui-lo a "interpretar" o tipo que você esta criando

 

Essa eu acho que é a exata definição do que acontece. Muito bom.

 

Mais tarde eu vou postar um programa de exemplo que eu escrevi, para mostrar isso funcionando numa implementação de uma classe bem simples. Mas vou implementar alguns operadores e uma exceção e umas rotinas de teste

Postado

Em geral não se quer de jeito nenhum redefinir operadores. Sobrecarregar operadores. De jeito nenhum. É um porre e você tem que tomar muitos cuidados e documentar bem o que está fazendo.  Documentar para você e para os outros.


Ao ler um programa a gente tem uma expectativa em relação aos operadores, como você tinha inicialmente, e quando não é mais isso pode dar muita confusão. Por outro lado se você cria uma classe e alguém vai lá e tenta operar com ela pode dar um problema enorme.


Quase na totalidade das vezes que se faz isso é para no mínimo redefinir os operadores == = e != como eu expliquei antes. 
Isso porque pode ser que ninguém queira multiplicar duas instâncias de Cadastro mas pode muito bem ser que alguém acabe comparando...

Cadastro     cadastro_a;
Cadastro     cadastro_b;
cadastro_b = cadastro_a * cadastro_b;    // difícil 
if( cadastro_a == cadastro_b )...    // bem mais provavel

 E se você tem estruturas de dados ou aloca memória nessa classe Cadastro quase certamente essa comparação não vai funcionar e você vai ter sim que escrever

  • código para o operador == e !=
  • código para o operador =
  • construtor de cópia para quando alguém usar Conjunto a = b + c; por exemplo
  • destrutor para liberar a memória no final da vida da instância
     

Mas e aí? Melhor esquecer?
Não. De modo algum. E muitas vezes não dá mesmo.

 

Teria eu um exemplo então?
Sim. Agora que --- eu acho -- o princípio ficou claro, vou mostrar um exemplo que pode ajudar a escrever código para isso, e entender quando escrever. Talvez.


Tenha paciência. Pode ser algo interessante. Eu por exemplo gostaria muito de ter lido um programa assim antes das primeiras vezes em que usei essa  #$%#%$#%%

 

Vou usar uma classe Conjunto e redefinir uns operadores e mostrar os resultados ou a falta deles.

class Conjunto
{
private:
    set<int>              elementos;
    string                nome;
    unsigned short        tamanho;

public:
    Conjunto() : tamanho{ 0 }, nome("conjunto"){}        // sem parametros
    Conjunto(string str) : tamanho{ 0 }, nome(str){}     // define nome
    Conjunto(string str, short primeiro, short limite, short incremento) : tamanho{ 0 }, nome(str)
    {
        for (int i = primeiro; i <= limite; i += incremento)
        {
            insere(i);
        }    // end for
    }    // construtor de exemplo, tipo um for: insere os caras

    void            aborta();
    void            define_nome(string);
    void            insere(int);
    string const    ve_nome() const;
    int const       ve_tamanho() const;

};    // end class Conjunto

Bem simples: é só um conjunto. De int. Na biblioteca padrão tem uma classe Set que é muito legal e é isso, um conjunto. Em java também tem, e em outras linguagens, porque é uma estrutura importante. Vou usar um conjunto de int que é como um vetor. Um conjunto de int é isso: um punhado de int ordenadinhos e sem repetição.

 

No nosso caso cada Conjunto tem

  • um nome
  • um tamanho
  • uma lista de elementos, todos int. Nào faz diferença de que modo eles estão gravados. Esse conceito é MUITO importante em programação com objetos e se chama encapsulamento

História curta: você pode usar a classe sem saber como eles estão guardados lá dentro. E estão declarados dentro daquele trecho 
private:
então você não pode nem mexer. Nem copiar. Nada.


E os métodos?
aborta() - faz o óbvio: derruba seu programa. Como dividir por zero. Outro conceito MUITO importante: exceção --- EXCEPTION.
define_nome() - permite que você altere o nome do Conjunto, já que você não tem acesso direto a isso.
ve_nome() - devolve o nome do conjunto, pela mesma razão: você não tem acesso porque foi declarado como private.
ve_tamanho() - faz o óbvio: devolve o tamanho do conjunto. 
insere() - faz o óbvio: insere o int lá, na posição certinha. Se ele já não estava lá

 

Só isso. Não vou implementar uma classe todinha funcional porque não vai fazer falta. Continue lendo.

Contruindo Conjunto: o construtor da classe.
Por definição na classe Conjunto qualquer função --- método é o nome usado --- declarado Conjunto() é chamada construtor e o compilador procura e chama isso quando você declara uma variável da classe. Quando tem mais de um ele se vira para achar o certo. Quando não tem nenhum ele cria um bem simples. Temos 3:

    Conjunto()           : tamanho{ 0 }, nome("conjunto"){}        // sem parametros
    Conjunto(string str) : tamanho{ 0 }, nome(str){}    // define nome
    Conjunto(string str, short primeiro, short limite, short incremento)
                         :tamanho{ 0 }, nome(str)

Então posso usar

Conjunto a();    // vai rodar o primeiro e cria um conjunto vazio com o nome "conjunto"
Conjunto b("C++"); // vai rodar o segundo e criar um conjunto com o nome "C++"
Conjunto c("conjunto 10", 1,10,1);    
// vai gerar o conjunto "conjunto 10" e colocar os int de 1 a 10 lá dentro 

Esse último é legal porque queremos testar o programa e não ficar digitando dados e assim 

Conjunto d("meio milhao", 1,1000000,2);    // impares ate 1 milhao, bem besta

Mas aí você quer listar o conjunto... Algo assim

        Conjunto    c;
        Conjunto    d("D");
        Conjunto    e("E 2 ate 20 de 2 em 2", 2, 20, 2);

        cout << c.ve_nome() << " tem agora " << c.ve_tamanho() << " elemetos" << endl;
        c.define_nome("outro");
        cout << c.ve_nome() << " tem agora " << c.ve_tamanho() << " elementos" << endl;

para ver se a função que muda o nome está ok e tal. E claro que vamos querer listar o conteúdo.

Mas para cada Conjunto vou ter que escrever muitos desses cout...  Muito chato

(Nem escrevi as funções que acessam o conteúdo ainda porque não é o que quero mostrar).

Eu poderia escrever uma função  mostra() dentro da classe Conjunto  e aí seria só chamar c.mostra(), d.mostra() e tal...

 

Mas e se eu pudesse escrever cout << c << endl; e funcionasse já? 

 

algo assim no programa

        Conjunto    e("22 caras", 1, 22, 1);
        cout << e;

e mostrasse isso:

 Conjunto: '22 caras'

     Elementos:....22

{
     1     2     3     4     5
     6     7     8     9    10
    11    12    13    14    15
    16    17    18    19    20
    21    22
}

Mais ainda, algo assim

        Conjunto    e("2 pares",   2, 4, 2);
        Conjunto    f("2 impares", 1, 3, 1);
        cout << e << f << endl << "encadeados tambem funciona sem fazer mais nada" << endl;;

Mostraria

Conjunto: '2 pares'

     Elementos:....2

{
     2     4
}

Fim do conjunto '2 pares'

Conjunto: '2 impares'

     Elementos:....3

{
     1     2     3
}

Fim do conjunto '2 impares'  
encadeados tambem funciona sem fazer mais nada

Bem favorável, certo? Pois é: essa é uma razão para redefinir o operador << nesse caso: eu posso usar no meio de um cout por exemplo e funciona direitinho e se eu quiser mudar o formato ainda  mudo em um único lugar.


E como faz essa p#%%@?  Bem, veja a linha

 

cout << e << endl;


E vamos ver o que tem lá. o IDE em geral até já mostra a declaração: basta deixar o mouse sobre o nome. cout é do tipo std::ostream e por isso você declarou o

#include <iostream> 

entre outras coisas.
E e  --- é óbvio --- é instância da classe  Conjunto declarada na linha de cima.
Então esse operador que queremos definir é algo como

    ostream& operator<< (ostream&, const Conjunto&);

Em C++ é um método que pega uma referência a ostream e uma referência a conjunto, e retorna uma referência a ostream. Um ponteiro. Ou quase isso. Esses const reforçam o fato de que nosso método não pode por a mão nem no conjunto nem na stream. Não faria sentido.
Com esse método declarado o compilador sabe que ao encontrar 

cout << e; 

vai ter que chamar nosso método.
Só isso.
Para esse código

        Conjunto    f("teste", 10, 30, 1);
        cout << f;

Mostrar isso

Conjunto: 'teste'

     Elementos:....21

{
    10    11    12    13    14
    15    16    17    18    19
    20    21    22    23    24
    25    26    27    28    29
    30
}

Fim do conjunto 'teste'

Foi usado esse método no programa de teste, que inclui as chaves e formata tudo

ostream& operator<<(ostream& o, const Conjunto& conjunto)
{
    o << endl;
    o << "Conjunto: '" << conjunto.nome << "'" << endl << endl;
    o << "     Elementos:...." << conjunto.tamanho << endl;
    // estara vazio?
    if (conjunto.elementos.size() == 0)    return o;

    o << endl << "{" << endl;
    int n = 0;

    for (auto i : conjunto.elementos)
    {
        o << setw(6) << i;
        n = n + 1;
        if (n % 5 == 0) cout << endl;
    }
    o << endl << "}" << endl;
    o << endl << "Fim do conjunto '" << conjunto.nome << "'" << endl << endl;
    return o;
}    // end operator<<

E se eu quisesse o produto?

c = a * b para a,b e c da classe Conjunto?

Faria sentido? Talvez não. Mas a classe é nossa e em Teoria de Conjuntos se pode imaginar o produto dos dois conjuntos como sendo um novo conjunto onde estão todos os possíveis produtos entre os elementos de a e b. Produto cartesiano se chama. O tamanho do conjunto final vai depender dos elementos porque não tem duplicata no nosso Conjunto. E o nome podia ser indicador do produto, algo assim:
Para

        Conjunto    c("3", 1, 3, 1);
        Conjunto    d("2", 1, 2, 1);      
        cout << c << d << c * d << endl;

Seria algo assim

Conjunto: '3'

     Elementos:....3

{
     1     2     3
}

Fim do conjunto '3'


Conjunto: '2'

     Elementos:....2

{
     1     2
}

Fim do conjunto '2'


Conjunto: '3*2'

     Elementos:....5

{
     1     2     3     4     6

}

Fim do conjunto '3*2'

Nada mal certo? e se pode escrever dentro do cout sem mudar nada. e até o nome está certinho e o total de elementos corrigido. e o 2 que seria gerado por 2*2 e já tinha no conjunto não ficou duplicado... E nem precisei declarar outro conjunto. E nem por parenteses. Faz sentido.

 

E como declarar o operador de multiplicação?


Veja a linha que nos interessa: 

 cout << c << d << c * d << endl;	// so interessa mesmo c * d

Então a operação com o operando :D :D  retorna um novo Conjunto a partir de dois conjuntos a e b que saem inalterados. Parece simples. Algo assim

Conjunto operator*(const Conjunto& a, const Conjunto& b)
{
    Conjunto produto(a.nome + "*" + b.nome);
    for (auto x : a.elementos)
    {
        for (auto y : b.elementos)
        {
            produto.elementos.insert(x * y);
        }
    }    // produto cartesiano
    produto.tamanho = (unsigned short) produto.elementos.size();
    return produto;
}    // end operator*
);

E não é que funciona?

E agora? E para dividir por exemplo? Sei lá. Já vimos o suficiente. Se define um comportamento seguro para o operador, define o método e segue em frente. Isto aqui é só um exemplo e não vou definir todos os operadores aqui.

  • No programa de teste vou implementar conjunto + int que simplemnte insere o int no conjunto se ele ainda não estiver lá, como a = a + 300;
  • e lógico int + conjunto porque não dá pra garantir em que sentido vai estar no programa isso e tem que funcionar para a = 300 + a;
  • E por consequência vai funcionar para a = 150 + a + 151.  Note que isso vai inserir os dois int no Conjunto! Somar conjuntos não é somar int
  • a = a + b para Conjunto simplesmente soma os valores, é a união entre os conjuntos

Esses estão no programa de teste

    friend ostream& operator<<(ostream&, const Conjunto&);
    friend Conjunto operator+(const Conjunto&, const Conjunto&);
    friend Conjunto operator+(const Conjunto&, const int);
    friend Conjunto operator+(const int, const Conjunto&);
    friend Conjunto operator*(const Conjunto&, const Conjunto&);
    friend Conjunto operator/(const Conjunto&, const Conjunto&);

Esse comando friend faz com que o método possa ter acesso às variáveis da classe Conjunto. Já discutimos << * e os +. Vou discutir algo sobre o / e encerrar por aqui. Escreva se tiver alguma dúvida. No final estará o link para o programa de teste e lá tem um botão de download e tal.
Note que com o que temos até aqui dá pra escrever 

        Conjunto    c("3", 1, 3, 1);
        Conjunto    d("2", 1, 2, 1);
        Conjunto    produto = c * d;

        Conjunto expressao = (d + d) * c + 300;    // nada mal

        cout << c;
        cout << d;
        cout << expressao << endl;

E em meia hora a classe Conjunto aprendeu essas várias funções, associativas como os operadores tem que ser. E tem o << que ajuda bem a mostrar os valores sem escrever mais códigos...

 

Veja o resultado

Conjunto: '3'

     Elementos:....3

{
     1     2     3
}

Fim do conjunto '3'


Conjunto: '2'

     Elementos:....2

{
     1     2
}

Fim do conjunto '2'


Conjunto: ''2' + '2'*3'

     Elementos:....6

{
     1     2     3     4     6
   300
}

Fim do conjunto ''2' + '2'*3'

Talvez ninguém vá ler até aqui :D

 

O que fazer com o / que até aqui não tem sentido?

 

Simples: cancelar o programa. Mas talvez de uma maneira educada. Assim damos a oportunidade de um usuário de nossa classe tratar o erro e continuar a vida, ou cancelar o programa de vez até ele desistir ou a gente implementar algum tipo de divisão de conjuntos :)

Veja o código para divisão como está no programa de teste:

Conjunto operator/(const Conjunto& a, const Conjunto& b) 
{
    a.aborta();
    return a;
}    // end operator/

Pois é: chama aborta(). Tanto faz chamar a.aborta() ou b.aborta() porque o programa vai abortar mesmo.

E essa aborta()?

void  Conjunto::aborta() const
{
    throw Erro_op_Conjunto();
}

Que é isso? Criamos uma exceção para nossa classe, Erro_op_Conjunto. E e se alguém tentar usar algum operador não implementado a gente pode:

  • interceptar no programa e tratar
  • cancelar como no caso de uma divisão por zero.

Pra que? Porque não faz sentido continuar.


E como faz isso? Criando uma nova classe a partir da classe exception do C++

class Erro_op_Conjunto : public exception
{
public:
    virtual const char* what() const throw()
    {
        return "Conjunto: Operacao Invalida ou Nao implementada ainda";
    }
};    // end Erro_op_Conjunto()

E como trata no programa?  Algo assim

int teste_outro()
{
    try
    {
        Conjunto    c("3", 1, 3, 1);
        Conjunto    d("2", 1, 2, 1);
        Conjunto    produto = c * d;
        Conjunto expressao = (d + d) * c + 300;
        cout << c;
        cout << d;
        cout << expressao << endl;
        cout << c / d;        // vai cancelar aqui
    }
    catch (Erro_op_Conjunto& excecao)
    {
        cout << excecao.what() << endl;
        cout << "Erro de operacao em Conjunto foi capturado: retorna normalmente" << endl;
    }    // end try
    return 0;
}

Esse não é um exemplo completo, e deve ter até erros. Mas mostra alguns dos conceitos de overloading e um pouco da prática de usar isso.
Me dei ao trabalho de postar isso porque eu quando precisei nunca achei algo escrito pequeno e comentado e tal pra eu testar... E eu tinha isso quse pronto na verdade

Se leu até aqui e vai rodar o programa de teste em seu computador, não me diga que ele está dando erro :D Foi feito assim.

Eis main()

int main(int argc, char** argv)
{
    teste_outro();
    teste_trata_excecao();
    cout << "***** voltou do teste *****" << endl;
    cout << "***** agora testa sem tratar erro em Conjunto *****" << endl;
    teste_nao_trata_excecao();
    cout << "***** voltou do teste *****" << endl;
    return 0;
}    // end main()

O programa de teste e um exemplo da saída está aqui, na pasta conjunto

E o programa pode ser lido  neste endereco

  • Curtir 1
  • Obrigado 1
Postado

Rapaz é realmente incrível o que se pode fazer sobrecarregando o << para formatar texto pelo ostream, esse sem duvida é um operador que vale a pena sobrecarregar, ele produz uma diferença bastante significativa para o código.

 

Notei que você adotou o uso mais convencional dos operadores, como + para soma, * para multiplicação, isso melhora bem o entendimento do programa, imagina o furdúncio que deve ser ver algo que vai contra a notação matemática e pelo que entendi o C++ permite esse uso de "operadores obscuros".

 

Muito obrigado por compartilhar seu conhecimento, material assim não é fácil conseguir ainda mais no nosso idioma.

Postado

 

14 horas atrás, Benjamin Breeg disse:

Rapaz é realmente incrível o que se pode fazer sobrecarregando o << para formatar texto pelo ostream, esse sem duvida é um operador que vale a pena sobrecarregar, ele produz uma diferença bastante significativa para o código

 
Não acha mais impressionante ainda essa linha abaixo funcionar?

Conjunto c,d;
Conjunto expressao =  a^b + (d + d) * c + 300;

Considerando que a, b, c e d são Conjunto --- conjuntos de int ordenados e escondidos dentro da classe Conjunto? :) . E a classe conjunto foi implementada usando discretamente a classe Set da STL (^ é o operador de intersecção, que eu criei ontem)?

 

14 horas atrás, Benjamin Breeg disse:

você adotou o uso mais convencional dos operadores, como + para soma, * para multiplicação, isso melhora bem o entendimento do programa

 

Sim, mas nesse caso a soma é a União de conjuntos e o produto é o produto cartesiano dos dois conjuntos. E algumas operações não fazem sentido se você ver bem:

  • A + 0 é diferente de A a menos que já tenha um zero no conjunto A
  • ^ pode ser considerado obscuro porque ao ler isso você não saberia que se trata da intersecção
  • Se a = b então a!=b Isso porque ao copiar a para b no nosso código o nome do conjunto b fica "b = 'a'"indicando que ele foi copiado, e então eles não são mais iguais: Sem pirataria na classe Conjunto
14 horas atrás, Benjamin Breeg disse:

pelo que entendi o C++ permite esse uso de "operadores obscuros"

 

Em muitos casos os operadores não fazem sentido para a classe e então se pode utilizar os tais operadores para descrever qualquer operação associativa que seria difícil de escrever com funções convencionais. E aí se você lê o programa focado no conceito de que + é mais e ++ é incremento pode se surpreender mesmo...

 

Uma classe que reproduz áudio poderia usar >  e >> para aumentar a velocidade de reprodução por exemplo... 

 

Quando se cria uma classe assim pode ser interessante declarar a classe como final para que não possa ser derivada. já que o código nas classes derivadas pode comprometer a lógica desses operadores.

 

O operador ^ pode ser  a intersecção de conjuntos...

Faz sentido implementar esse operador para a interseção de conjuntos A e B

 

Então seria

Conjunto A;
Conjunto B;

cout A^B >> endl;

deve mostrar todos os elementos que estão em A e B e só esses....

 

Entendeu o lance da exceção para interceptar o uso de operadores sem função?

 

Os links do programa e da pasta agora estão públicos. Estavam na parte privada do GitHub. Foi mal. Eu não tinha visto porque meu link já entrava com a senha...

 

Lista dos operadores que podem e não podem ser sobrecarregados, direto de cplusplus.org

 

ch-191002-listaoperar.jpg.2b86d2c57b0e558f976a268780cbde9d.jpg

 

Inclui alguns dos operadores que faltavam e gerei um novo programa de teste com uma rotina mais formal.

 

Pode ser uma boa leitura porque é um inferno achar uma referência para essas coisas. Ou eu não sei procurar...

O programa está em aqui, e tem um botão pra download no formato zip

Ou pode ser lido direto em aqui

Um exemplo da saída está aqui

  • Obrigado 1
Postado

Eu não tinha usado exceções ainda mas entendi se a função encontra um erro que não pode tratar, ela não vai usar o retorno convencional com return, ela usa o thrown para lançar a exceção que vai indicar o que provocou o erro para ser capturado com o catch que vai acionar um ou mais "tratadores" dentro daquele bloco especifico..

 

Exemplo bobo que achei com o clássico erro de divisão por zero usando exceções.

 

#include <iostream>

using namespace std;

float divide(float x, float y)
{
    if (y == 0) {
        throw 1;
    } else {
        return x/y;
    }
}

int main()
{
    int a, b;
    bool error = false;
    cout << "Digite os valores: ";
    cin >> a >> b;

    do {
        try {
            cout << divide(a, b);
            error = false;
        } catch(int erro) {
            if (erro == 1) {
                cout << "Erro por divisao por zero. Digite outro valor: ";
                cin >> b;
            }
            error = true;
        }
    } while (error);

    return 0;
}

 

Consegui baixar o programa e os links que você enviou. Muito obrigado.

Postado
21 horas atrás, Benjamin Breeg disse:

ela não vai usar o retorno convencional com return, ela usa o thrown para lançar a exceção que vai indicar o que provocou o erro para ser capturado com o catch que vai acionar um ou mais "tratadores" dentro daquele bloco especifico

 

Exceções são assim ,exceções. No geral não se recomenda ou usa isso porque sai muito caro em termos de recursos e tempo de execução. 

Mas no caso dessa classe exemplo Conjunto achei que faria sentido para interceptar operações inválidas como por exemplo a divisão de Conjunto, que não foi implementada ainda. E nesse caso é uma exceção definida pelo usuário, criando uma classe nova a partir da classe exception e reescrevendo a função padrão what()  para permitir ao usuário da classe tratar a exceção se quiser. Mas o principal é mostrar uma maneira de IMPEDIR o uso do operador /

 

21 horas atrás, Benjamin Breeg disse:

Exemplo bobo que achei com o clássico erro de divisão por zero usando exceções

 

Bem bobo: divisão por zero não é uma exceção em C++ e é muito mais simples tratar o erro testando antes o dividendo.

 

Vou deixar um outro exemplo aqui, meio fora do tópico mas por estar ainda no contexto da discussão. 

 

Um programa que gera e intercepta algumas --- seis --- exceções do C++ 

 

Saida

Teste 0
OUT_OF_RANGE: invalid string position
Teste 1
BAD_ARRAY: bad array new length
Teste 2
UNDERFLOW: E agora?
Teste 3
Deu pau em outra coisa...
Teste 4
tentando alocar -1 bytes
BAD_ARRAY: bad array new length
Teste 5
tentando alocar 1000000000 bytes
bad_alloc: bad allocation

O programa

#include<exception>
#include <iostream>
using namespace std;

class Aloca
{
private:
    int* buffer;
    int        tamanho;
public:
    Aloca(int t) : tamanho(t)
    {
        cout << "tentando alocar " << t << " bytes" << endl;
        buffer = new int[tamanho];
    }
};  // end class


int main( int argc, char** argv )
{
    string        pequena_string = "pequena";
    int            i = 0;
    do {
        try
        {
            switch (i)
            {
            case 0:
                cout << "Teste " << i << endl;
                i++;
                cout << pequena_string.substr(700) << endl;
                break;
            case 1:
            {
                cout << "Teste " << i << endl;
                i++;
                int z = -1;
                char* vetor = new char[z];
                break;
            }
            case 2:
            {
                cout << "Teste " << i << endl;
                i++;
                throw std::underflow_error("E agora?");
                break;
            }
            case 3:
            {
                cout << "Teste " << i << endl;
                ++i;
                throw 32;
            }
            case 4:
                cout << "Teste " << i << endl;
                i++;
                Aloca(-1);
                break;
            case 5:
                cout << "Teste " << i << endl;
                i++;
                Aloca(1000000000);
                break;
            default:
                exit(0);
            }    // end switch
        }
        catch (std::out_of_range & e)
        {// OK
            std::cout << "OUT_OF_RANGE: " << e.what() << endl;
        }
        catch (std::invalid_argument & e)
        {// OK
            std::cout << "INVALID: " << e.what() << endl;
        }
        catch (std::length_error & e)
        {// OK
            std::cout << "LENGTH: " << e.what() << endl;
        }
        catch (std::bad_array_new_length & e)
        {// OK
            std::cout << "BAD_ARRAY: " << e.what() << endl;
        }
        catch (std::underflow_error & e)
        {// OK
            std::cout << "UNDERFLOW: " << e.what() << endl;
        }
        catch (std::bad_alloc & e)
        {
            std::cout << "bad_alloc: " << e.what() << endl;
        }
        catch (std::exception & e)
        {
            std::cout << "std::exception " << e.what() << endl;
            std::cout << "exception x " << i << endl;
        }
        catch (...)
        {
            std::cout << "Deu pau em outra coisa..." << endl;
        }    // end try{}
    } while (1);    // end do{}
    cout << "fim normal" << endl;
    return 0;
}

O programa sobrevive a erros de underflow, bad_alloc, argumentos inválidos e termina normalmente

  • Obrigado 1

Crie uma conta ou entre para comentar

Você precisa ser um usuário para fazer um comentário

Criar uma conta

Crie uma nova conta em nossa comunidade. É fácil!

Crie uma nova conta

Entrar

Já tem uma conta? Faça o login.

Entrar agora

Sobre o Clube do Hardware

No ar desde 1996, o Clube do Hardware é uma das maiores, mais antigas e mais respeitadas comunidades sobre tecnologia do Brasil. Leia mais

Direitos autorais

Não permitimos a cópia ou reprodução do conteúdo do nosso site, fórum, newsletters e redes sociais, mesmo citando-se a fonte. Leia mais

×
×
  • Criar novo...

GRÁTIS: ebook Redes Wi-Fi – 2ª Edição

EBOOK GRÁTIS!

CLIQUE AQUI E BAIXE AGORA MESMO!