| 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
| [ 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. |
|
|
|
| [ 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.
|
|
|
|
| [ 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. |
|
|
|
| [ 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
}
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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);
// ...
}
| [ 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(). |
|
|
|