paulo1205
(usa Ubuntu)
Enviado em 10/07/2015 - 19:52h
A palavra-chave é “encapsulamento”. Isso engloba o que já foi dito antes (poder implementar restrições, esconder detalhes da implementação), mas também envolve a questão de como chegar ao dado.
Quando se pensa em programação orientada a objetos, o
objeto deve ser o ponto de acesso para todos os elementos e operações que o compõem. Funções-membros (ou métodos), incluindo
getters e
setters, são maneiras de garantir que o ponto de entrada a será sempre um objeto.
Abaixo eu mostro um exemplo (muito simplório, até, pois só tem operações triviais) de como o acesso direto a membros de dados pode permitir a destruição do encapsulamento.
class A {
public:
int value;
};
int main(){
A *a=new A; // Ponteiro, só para o exemplo ficar mais drástico.
int &r=a->value;
int *p=&a->value;
a->value=5; // Acesso via objeto. OK.
r++; // Altera objeto sem mencioná-lo diretamente (mau!).
(*p)*=2; // Idem (via ponteiro) (mau!).
delete a; // NOTE BEM: Removi o objeto...
r++; // ... mas ainda estou modificando alguma coisa...
(*p)*=2; // ... que nem existe mais! (mau, muito mau!)
{
A aa; // Agora, sem ponteiro.
p=&aa.value; // Mas olha a maldade aqui.
}
// Neste ponto, aa não existe mais, mas p ainda ponta para o que seria parte dele.
(*p)++; // BOOM!
}
A questão do encapsulamento é particularmente relevante com objetos compostos por vários elementos internos, quando a manutenção da coerência entre esses elementos internos é vital para que o objeto seja consistente. Na verdade, classes bem implementadas têm de garantir que todos os objetos estejam completamente consistentes durante todo o seu tempo de vida, desde que ele termina de ser construído até o momento em que será des(cons)truído (e mesmo durante a construção e desconstrução, as coisas também deve ser feitas com ordem, e eventuais erros tratados de forma a manter o estado geral do programa consistente).
Aliás, por falar em construção e desconstrução, o construtor é um
setter por excelência, e também um
getter do objeto, tomado como um todo.
Veja este outro exemplo de um encapsulador para strings nativas do C com
std::string da biblioteca padrão do C++, mas permitindo também representar o equivalente a um ponteiro nulo do C (coisa que std::string não faz, e que é essencialmente diferente de usar simplesmente uma string vazia).
#include <fstream>
#include <iostream>
#include <string>
class c_str_wrapper {
public: /* Deveria ser "private:", mas é para ilustrar o perigo. */
std::string str;
bool is_valid;
public:
c_str_wrapper(const char *s){
if(s){
str=s;
is_valid=true;
}
else
is_valid=false;
}
// Os operadores de conversão de tipos, abaixo, são
// casos particulares de getters, mas, sozinhos, não
// garantem a consistência dos membros internos do
// objeto.
operator bool(){ return is_valid; }
operator const char *() const {
return is_valid? str.c_str(): nullptr;
}
};
bool print(const char *p){
std::cout << (p? (*p? p: "[[EMPTY_STRING]]"): "[[NULL_POINTER]]") << '\n';
return std::cout;
}
int main(){
std::cout.exceptions(std::fstream::failbit|std::fstream::badbit);
c_str_wrapper a{nullptr};
c_str_wrapper b{""};
c_str_wrapper c{"Teste"};
print(a); // Imprime "[[NULL_POINTER]]".
print(b); // Imprime "[[EMPTY_STRING]]".
print(c); // Imprime "Teste".
// Até aqui, tudo OK. A semântica está como esperado. Agora vamos
// começar a esculhambar.
a.is_valid=true;
b.is_valid=false;
c.is_valid=false;
print(a); // Imprime "[[EMPTY_STRING]]" porque casualmente o construtor
// default de std::string coloca um string vazio em a.str,
// mesmo quando ele, pela semântica original, não devesse ser
// usado.
print(b); // Imprime "[[NULL_POINTER]]".
print(c); // Imprime "[[NULL_POINTER]]".
const char *cp_a, *cp_b, *cp_c; // Ponteiros para caracteres constantes.
a.is_valid=b.is_valid=c.is_valid=true; // Todas as strings "válidas".
cp_a=a;
cp_b=b;
cp_c=c;
// Uso const_cast para burlar não só o encapsulamento de c_str_wrapper,
// mas também o de std::string (std::string::c_str() não é naturalmente
// muito segura, mas é algo que se tem de sofrer para poder ter compa-
// tibilidade com strings do C; ela pelo menos retorna um ponteiro para
// constantes, mas casts sempre permitem que a gente transforme qualquer
// coisa em outra).
char *p_a, *p_b, *p_c;
p_a=const_cast<char *>(cp_a);
p_b=const_cast<char *>(cp_b);
p_c=const_cast<char *>(cp_c);
p_a[0]='x'; // Removo o const e sobrescrevo o '\0' do fim da string
// do C usada internamente por std::string.
p_b[0]='y'; // Removo o const e sobrescrevo o '\0'.
p_c[5]='z'; // Idem (só que na sexta posição).
print(a); // Pode imprimir 'x' ou 'y', talvez seguido de lixo, mas pode
// também acontecer qualquer coisa, até capotar o programa.
print(b); // Idem.
print(c); // Pode imprimir "Testez", talvez seguido de lixo, mas pode
// também acontecer qualquer coisa, até capotar o programa.
// Mais "divertido" ainda: mexo em memória fora do espaço alocado pelo
// construtor de std::string para conter textos ou a representação com-
// patível com a do C.
p_a[10]='a';
p_b[10]='b';
p_c[10]='c';
// Mesmo que uma das operações acima não dê pau, pode ser que dê problema
// na hora de chamar um dos destrutores, o que acontece aqui.
}
A classe
c_str_wrapper poderia ser melhorada, do seguinte modo.
class c_str_wrapper {
private: /* Não mais "public:". */
std::string str;
bool is_valid;
public:
// Setter que dá ao objeto o sentido de ponteiro nulo.
// Note que ele mexe em mais do que apenas um campo.
void set_null(){
str.clear();
is_valid=false;
}
// Outro setter, que recebe C-string. Note que ele também
// tem de ser esperto.
void set_str(const char *s){
if(s){
str=s;
is_valid=true;
}
else{
str.clear();
is_valid=false;
}
}
// O setter acima é tão adequado ao objeto que o próprio
// construtor de conversão de tipos pode usá-lo.
c_str_wrapper(const char *s){ set_str(s); }
// O operador de conversão de tipo abaixo é um caso
// particular de getter. Uma função equivalente segue.
operator bool() const { return is_valid; }
bool is_null() const { return is_valid; }
// Getter da string, que retorna uma cópia de str ou, se
// o dado não for considerado válido, gera uma exceção
// (note que o tipo de retorno é “std::string”, e não
// “std::string &”).
std::string get_str() const {
if(!is_valid)
throw std::runtime_error("Attempt to get an undefined string");
return str;
}
// O getter especial dado pela conversão de tipos para
// const char * foi removido. Em seu lugar, deve-se usar algo
// como “obj.get_str.c_str()”.
// Outro tipo especial de getter, que coloca uma cópia num
// array do C.
void get_str(char *dst, size_t dst_size) const {
if(!is_valid)
throw std::runtime_error("Attempt to get an undefined string");
if(!dst)
throw std::invalid_argument("Attempt to write to null pointer");
if(dst_size<=str.length())
throw std::range_error("Destination buffer too small");
strcpy(dst, str.c_str());
}
};
// Também seria necessário escrever uma nova versão de print().
bool print(const c_str_wrapper &s){
if(!s) // getter implícito de conversao para bool
std::cout << "[[NULL_POINTER]]";
else{
std::string str(s.get_str());
std::cout << (s.empty()? "[[EMPTY_STRING]]": s);
}
return std::cout << '\n';
}
A eliminação de uma das conversões de tipos é compensada pelos outros
getters colocados.
Contudo, só o fato de usar
setters e
getters não é necessariamente uma garantia do encapsulamento. O tipo de dado devolvido por um
getter pode fazer toda a diferença. De modo geral, deve-se evitar fazer
getters que devolvam referências ou ponteiros para os membros da classe.
Mas mesmo isso ainda não é tudo. O tipo
std::string pode ser implementado com uma semântica de
copy-on-write, para fins de economia de memória e também de tempo de execução. Essa facilidade é realmente útil em muitas situações, mas acaba fornecendo potencial combustível para eventuais incendiários. Como dito num comentário no código acima, a função membro
c_str() devolve justamente um ponteiro para elemento interno do objeto, e esse elemento é justamente o que é compartilhado no mecanismo de
copy-on-write. Então, alguém que deliberadamente use
c_str() junto com mecanismos de baixo nível, pode acabar interferindo, de uma vez só, com múltiplos objetos.
int main(){
// Para maior dramatismo, uso o qualificador const para os objetos.
const c_str_wrapper a{"Teste"};
const c_str_wrapper b{a};
const c_str_wrapper c{a};
const_cast<char *>(a.get_str().c_str())[0]='P';
// Corremos o risco de imprimir "Peste" até três vezes
// (e de fato, é o que ocorre na minha máquina)!
print(a);
print(b);
print(c);
}
Esse efeito pode ser contornado por meio de substitutos para o construtor de cópia para o operador de atribuição gerados implicitamente para a classe pelo compilador. Essas versões explícitas devem forçar a geração de um objeto
std::string totalmente novo, em lugar de usar a atribuição que usa o recurso de
copy-on-write de um objeto string preexistente.
// Construtor de cópia.
c_str_wrapper(const c_str_wrapper &other) :
str(other.str.c_str()), // Note que eu NÃO fiz “str(other.str)”!!
is_valid(other.is_valid)
{
}
// Operador de atribuição.
c_str_wrapper &operator=(const c_str_wrapper &other){
str=other.str.c_str(); // De novo: eu NÃO fiz “str=other.str”!!
is_valid=other.is_valid;
return *this;
}
Com esses novos membros na classe, o programa anterior mexe apenas com o conteúdo de
a.
Há um custo a pagar, no entanto, por essas garantias: um número maior de operações de cópia de strings.
Se eu mexer na versão de
c_str_wrapper::get_str() que retorna
std::string, trocando o valor retornado de
str para
str.c_str(), aí já não vou mais conseguir burlar nem o valor de
a (e terei mais uma cópia de strings).