FAQ Lite
Sobrecarga de Operador

[ 13.1 ] O que caracteriza sobrecarga de operador?
[ 13.2 ] Quais são os benefícios de sobrecarregar o operador?
[ 13.3 ] Quais são alguns exemplos de sobrecarga de operador?
[ 13.4 ] Mas a sobrecarga de operador faz minha definição de classe parecer confusa. Não é um recomendável que eu faça meu código limpo?
[ 13.5 ] Quais os operadores que podem/não podem ser sobrecarregados?
[ 13.6 ] Posso sobrecarregar o operator ==, de modo que me permita comparar dois char[] usando uma comparação de string?
[ 13.7 ] Posso criar um operator** para operações de potência de dois?
[ 13.8 ] Como faço para criar um operator subscrito para a classe Matrix ?
[ 13.9 ] Devo definir minhas classes de fora para dentro, definindo primeiramente as interfaces, ou de dentro para fora, definindo primeiramente os dados?

[ 13.1 ] O que caracteriza "sobrecarga de operador"?

É o que lhe permite fornecer uma interface intuitiva para os usuários de sua classe.

Sobrecarga de operador permite que os operadores C/C++ tenham um significado definido pelo usuário, em tipos de dados também definidos pelo usuário (classes). Operadores sobrecarregados adoçam a sintaxe das chamadas de função:

    class Fred {
    public:
      // ...
    };
    
    #if 0
    
      // Without operator overloading:
      Fred add(Fred, Fred);
      Fred mul(Fred, Fred);
    
      Fred f(Fred a, Fred b, Fred c)
      {
        return add(add(mul(a,b), mul(b,c)), mul(c,a));    // Yuk...
      }
    
    #else
    
      // With operator overloading:
      Fred operator+ (Fred, Fred);
      Fred operator* (Fred, Fred);
    
      Fred f(Fred a, Fred b, Fred c)
      {
        return a*b + b*c + c*a;
      }
    
    #endif 
Topo
[ 13.2 ] Quais são os benefícios de sobrecarregar o operador?

Sobrecarregando os operadores padrões em uma classe, você pode explorar a intuição dos usuários da classe. Isso permite aos usuários programar na linguagem do problema e não na linguagem da máquina.

As metas básicas da sobrecarga de operador são reduzir a curva de aprendizado dos usuários para utilização das classes, e ainda diminuir a taxa de erros na aplicação final.

Topo
[ 13.3 ] Quais são alguns exemplos de sobrecarga de operador?

Aqui estão alguns exemplos de sobrecarga de operador:
  • myString + yourString concatena dois objetos string
  • myDate++ incrementa um objeto Date
  • a * b multiplica dois objetos Number
  • a[i] acessa uma elemento de um objeto Array
  • x = *p des-referencia um smart pointer que na realidade aponta para um registro em disco. Essa operação poderia, na realidade, procurar pela localização do disco para a qual p aponta e retornar, dentro de x, o registro encontrado naquela localização.
Topo
[ 13.4 ] Mas a sobrecarga de operador faz minha definição de classe parecer confusa. Não é um recomendável que eu faça meu código limpo?

Sobrecarga de operador torna a vida mais fácil para os usuários da classe, não para o desenvolvedor da classe.

Considere o seguinte exemplo:

    class Array {
    public:
      int& operator[] (unsigned i);      // Some people don't like this syntax
      // ...
    };
    
    inline
    int& Array::operator[] (unsigned i)  // Some people don't like this syntax
    {
      // ...
    } 
Há quem não goste da palavra-chave operator, nem de nenhum outro termo estranho colocado no corpo da classe. Mas a sobrecarga de operator não se destina a facilitar a vida do desenvolvedor da classe, e sim a dos usuários da classe.
    main()
    {
      Array a;
      a[3] = 4;   // User code should be obvious and easy to understand...
    } 
Lembre-se: No universo da orientação para reutilização de código, normalmente haverá várias pessoas que usarão suas classes, e somente uma que a construirá. Você mesmo. Portanto você deve fazer escolhas em benefício da maioria, e não em benefício de uns poucos.
Topo
[ 13.5 ] Quais os operadores que podem/não podem ser sobrecarregados?

A maioria pode ser sobrecarregada. O único operador C que não pode ser sobrecarregado é . e ?: (e sizeof, que já é, tecnicamente, um operator). C++ acrescenta uns poucos operadores próprios, a maioria dos quais pode ser sobrecarregado, exceto :: e .*

Aqui está um exemplo de operator subscrito (que retorna uma referência). O primeiro sem sobrecarga de operator:

    class Array {
    public:
      #if 0
        int& elem(unsigned i)        { if (i > 99) error(); return data[i]; }
      #else
        int& operator[] (unsigned i) { if (i > 99) error(); return data[i]; }
      #endif
    private:
      int data[100];
    };
    
    main()
    {
      Array a;
    
      #if 0
        a.elem(10) = 42;
        a.elem(12) += a.elem(13);
      #else
        a[10] = 42;
        a[12] += a[13];
      #endif
    } 
Topo
[ 13.6 ] Posso sobrecarregar o operator ==, de modo que me permita comparar dois char[] usando uma comparação de string?

Não. Pelo menos um operando, em qualquer operator sobrecarregado, deve ser do mesmo tipo da classe.

Mesmo que o C++ permitisse isso, e ele não permite, você não iria de qualquer modo querer fazer isso. Realmente você deveria usar uma classe tipo string ao invés de uma matriz de char, já que matrizes são miseráveis.

Topo
[ 13.7 ] Posso criar um operator** para operações de "potência de dois"?

Não.

Os nomes, precedências, propriedades de associatividade e o significado dos operadores são fixados pela linguagem. Não existe um operator** em C++, portanto você não pode criar um para um tipo class.

Se você está em dúvida, considere que x ** y é o mesmo que x * (*y), e aqui o compilador assume que *y é um pointer. Por outro lado, sobrecarga de operator é apenas um adoçante para as chamadas de função. Embora esse adoçante em particular seja bastante doce, ele não acrescenta nada de fundamental ao processo. Eu sugiro que você sobrecarregue pow(base,exponent). Uma versão em precisão dupla existe em <math.h>

A propósito, operator^ pode funcionar para potência de dois, com a ressalva de que ele tem a precedência e a associatividade incorretas.   

Topo
[ 13.8 ] Como faço para criar um operator subscrito para a classe Matrix ?

Use operator() ao invés de operator[].

Quando você tem múltiplos subscritos, o modo mais claro, limpo de fazer isso é com operator(), ao invés de operator[]. A razão é que operator[] sempre recebe exatamente um parâmetro, mas operator() pode receber qualquer número de parâmetros. No caso de uma matriz retangular, dois parâmetros são necessários.

Por exemplo:

    class Matrix {
    public:
      Matrix(unsigned rows, unsigned cols);
      double& operator() (unsigned row, unsigned col);
      double  operator() (unsigned row, unsigned col) const;
      // ...
     ~Matrix();                              // Destructor
      Matrix(const Matrix& m);               // Copy constructor
      Matrix& operator= (const Matrix& m);   // Assignment operator
      // ...
    private:
      unsigned rows_, cols_;
      double* data_;
    };
    
    inline
    Matrix::Matrix(unsigned rows, unsigned cols)
      : rows_ (rows),
        cols_ (cols),
        data_ (new double[rows * cols])
    {
      if (rows == 0 || cols == 0)
        throw BadIndex("Matrix constructor has 0 size");
    }
    
    inline
    Matrix::~Matrix()
    {
      delete[] data_;
    }
    
    inline
    double& Matrix::operator() (unsigned row, unsigned col)
    {
      if (row >= rows_ || col >= cols_)
        throw BadIndex("Matrix subscript out of bounds");
      return data_[cols_*row + col];
    }
    
    inline
    double Matrix::operator() (unsigned row, unsigned col) const
    {
      if (row >= rows_ || col >= cols_)
        throw BadIndex("const Matrix subscript out of bounds");
      return data_[cols_*row + col];
    } 
Assim você pode acessar um elemento de Matrix m usando m(i,j) ao invés de m[i][j]:
    main()
    {
      Matrix m;
      m(5,8) = 106.15;
      cout << m(5,8);
      // ...
    } 
Topo
[ 13.9 ] Devo definir minhas classes de fora para dentro, definindo primeiramente as interfaces, ou de dentro para fora, definindo primeiramente os dados?

De fora para dentro!

Uma boa interface fornece uma visão simplificada, que é expressa no vocabulário do usuário. No caso de software OO, a interface é projetada para uma classe em particular, ou para um grupo compacto de classes.

Primeiro pense no que o objeto representa logicamente, e não em como você pretende implementá-lo fisicamente. Por exemplo, suponha que você tenha uma classe Stack que será implementada contendo LinkedList.

    class Stack {
    public:
      // ...
    private:
      LinkedList list_;
    }; 
Stack deveria ter um método get() que retorna a LinkedList? Ou um método set() que recebe a LinkedList? Ou um construtor que recebe a LinkedList? Obviamente a resposta é não, já que você deve definir sua interface de fora para dentro. Neste caso, os usuários de Stack não têm qualquer interesse em Linked List. eles se interessam por colocar e retirar (elementos em Stack).

Agora um outro exemplo um pouco mais sutil. Suponha que a classe LinkedList é implementada usando uma lista ligada de objetos Node, onde cada Node tem um pointer para o próximo Node.

    class Node { /*...*/ };
    
    class LinkedList {
    public:
      // ...
    private:
      Node* first_;
    }; 
A classe LinkedList deveria ter um método get() que permita aos usuários acessar o primeiro Node? O objeto Node deve ter um método get() que permita aos usuários seguir de um Node para o próximo Node ao longo da cadeia? Em outras palavras, como LinkedList deve ser, deve se apresentar, deve parecer de um ponto de vista externo? LinkedList é realmente uma cadeia de objetos Node? Ou a cadeia de objetos é apenas um detalhe de implementação? Se é apenas um detalhe de implementação, como LinkedList vai permitir aos usuários acessar cada um dos membros de LinkedList, um de cada vez?

Um homem responde: Uma LinkedList não é uma cadeia de Nodes. Isso é como ela é implementada, mas não é o que ela é, em essência. Qual a seqüência dos elementos? De qualquer modo, a abstração de LinkedList deve fornecer também uma class LinkedListIterator, e tal LinkedListIterator deve ter um operator++ para ir para o próximo elemento, e ainda deve ter um par get()/set() para acessar valores armazenados no Node (o valor dentro do elemento Node é responsabilidade exclusiva do usuário de LinkedList, aí está porque tem que haver um par get()/set() que permita ao usuário manipular livremente os valores)

Começando pela perspectiva do usuário, devemos querer que nossa LinkedList suporte operações em tudo semelhantes às operações com matrizes, usando pointers aritiméticos.

    void userCode(LinkedList& a)
    {
      for (LinkedListIterator p = a.begin(); p != a.end(); ++p)
        cout << *p << '\n';
    } 
Para implementar a interface que se deseja, LinkedList vai precisar de um método begin() e de um método end(). Este retorna um objeto LinkedListIterator. O LinkedListIterator vai precisar de um método para deslocar-se para frente, ++p; um método para acessar o elemento corrente, +p; e ainda um operador de comparação. p!= a.end().

O código é apresentado a seguir. O aspecto chave é que LinkedList class não tem qualquer método que permita ao usuário acessar os Nodes. Nodes são, aqui, uma técnica de implementação completamente oculta do usuário. A LinkedList class poderia ter suas partes internas substituídas por uma dupla lista ligada, ou mesmo por uma matriz, e a única mudança perceptível seria alguma  diferença de performance nos métodos prepend(elem) e append(elem).

    #include <assert.h>   // Poor man's exception handling
    
    typedef  int  bool;   // Someday we won't have to do this
    
    class LinkedListIterator;
    class LinkedList;
    
    class Node {
      // No public members; this is a "private class"
      friend LinkedListIterator;   // A friend class
      friend LinkedList;
      Node* next_;
      int elem_;
    };
    
    class LinkedListIterator {
    public:
      bool operator== (LinkedListIterator i) const;
      bool operator!= (LinkedListIterator i) const;
      void operator++ ();   // Go to the next element
      int& operator*  ();   // Access the current element
    private:
      LinkedListIterator(Node* p);
      Node* p_;
    };
    
    class LinkedList {
    public:
      void append(int elem);    // Adds elem after the end
      void prepend(int elem);   // Adds elem before the beginning
      // ...
      LinkedListIterator begin();
      LinkedListIterator end();
      // ...
    private:
      Node* first_;
    }; 
Aqui estão os métodos que são obviamente elegíveis para uso inline (provavelmente no mesmo arquivo-header).
 inline bool LinkedListIterator::operator== (LinkedListIterator i) const
    {
      return p_ == i.p_;
    }
    
    inline bool LinkedListIterator::operator!= (LinkedListIterator i) const
    {
      return p_ != i.p_;
    }
    
    inline void LinkedListIterator::operator++()
    {
      assert(p_ != NULL);  // or if (p_==NULL) throw ...
      p_ = p_->next_;
    }
    
    inline int& LinkedListIterator::operator*()
    {
      assert(p_ != NULL);  // or if (p_==NULL) throw ...
      return p_->elem_;
    }
    
    inline LinkedListIterator::LinkedListIterator(Node* p)
      : p_(p)
    { }
    
    inline LinkedListIterator LinkedList::begin()
    {
      return first_;
    }
    
    inline LinkedListIterator LinkedList::end()
    {
      return NULL;
    } 
Conclusão: A lista ligada tem duas espécies de dados. Os valores dos elementos armazenados na lista ligada são responsabilidade do usuário da lista ligada (e apenas do usuário; a lista ligada por si mesma não toma qualquer ação para  impedir o usuário de alterar o terceiro elemento para 5), e a infraestrutura da lista ligada (pointer next, etc), cujos valores são responsabilidades da lista ligada (e apenas da lista ligada, ou seja, a lista ligada não permite ao usuário alterar, ou mesmo ver, os pointers next.

Os únicos métodos get()/set() servem para obter e armazenar elementos na lista ligada, mas não na infraestrutura da lista ligada.

Uma vez que a lista ligada oculta sua infraestrutura, pointers, etc., é possível fazer promessas muito firmes sobre essa infraestrutura. Por exemplo, se ela for uma lista ligada dupla, ela tem que garantir que cada pointer para o Node adiante seja coincidente com um pointer para um Node anterior)

Então nós vimos aqui um exemplo de onde os valores de alguns dados da classe são responsabilidade dos usuários (e nesses casos a classe precisam ter métodos get()/set() para os dados), além de dados de controle da classe, para os quais não há necessariamente métodos get()/set().

Topo Anterior Próximo Índice
C++ FAQ Lite
Copyright © 1991-98 by Marshall Cline Ph.D., cline@parashift.com
Tradução: Dagoberto Haele Arnaut

| Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site |