Evitando "efeitos colaterais indesejáveis" com "const correctness"

There are only two kinds of languages: the ones people complain about and the ones nobody uses.
Bjarne Stroustrup

Poucas linguagens permitem ao programador expressar suas intenções, manipulando dados em memória, com tanta riqueza quanto C++. Exemplo disso, é uma funcionalidade da linguagem, extremamente poderosa, ausente na maioria das linguagens mainstream, conhecida como const correctness.

Mais do mesmo…

Para entender o poder de const correctness, vamos, antes, revisitar duas features mais simples, comuns a quase todas as linguagens: passagem de parâmetros “por valor” e “por referência”

No exemplo que segue, uma string é passada “por valor”. Ou seja, um “objeto-cópia” é criado quando a função é acionada e destruído no momento em que ela retorna.

#include <iostream>
#include <string>

// s is passed by value
void f(std::string s) {
  s.clear();
  std::cout << "F: S is empty: " << s.empty() << std::endl;
}

int main() {
  std::string s("This is my string");
  f(s);
  std::cout << "main: S is empty: " << s.empty() << std::endl;
  std::cout << "main: S value   : " << s << std::endl;
  return 0;
}

// OUTPUT
// F: S is empty   : 1
// main: S is empty: 0
// main: S value   : This is my string

O resultado desse código é que a string original, criada na função main, permanece seguramente inalterada, enquanto uma nova string é manipulada na função f .

Já no exemplo que segue, é indicado que o valor recebido como parâmetro é passado “por referência”.

#include <iostream>
#include <string>

// s is a reference
void f(std::string& s) {
  s.clear();
  std::cout << "F: S is empty: " << s.empty() << std::endl;
}

int main() {
  std::string s("This is my string");
  f(s);
  std::cout << "main: S is empty: " << s.empty() << std::endl;
  std::cout << "main: S value   : " << s << std::endl;
  return 0;
}

// OUTPUT
// F: S is empty   : 1
// main: S is empty: 1
// main: S value   : 

O resultado do código acima é que a string original, criada na função main, é a mesma recebida e manipulada na função f .

Um problema quase sem solução, fora do C++

Como previnir que um programador realize alterações, inadvertidamente, no estado de um objeto passado como parâmetro “por referência”? Em C++, basta marcar tal parâmetro como const.

#include <iostream>
#include <string>

// s is a reference
void f(const std::string& s) {
  s.clear();
  std::cout << "F: S is empty: " << s.empty() << std::endl;
}

int main() {
  std::string s("This is my string");
  f(s);
  std::cout << "main: S is empty: " << s.empty() << std::endl;
  std::cout << "main: S value   : " << s << std::endl;
  return 0;
}

O que acontece aqui é uma espécie de “casting” do tipo do objeto passado como parâmetro, onde todos os membros não marcados como const tornam-se inacessíveis. Qualquer violação é detectada em tempo de compilação, sem prejuízos de performance em tempo de execução.

Como o programador expressa “membros const” em seus tipos

Cabe ao programador, nas suas implementações de tipos, como a indicada no exemplo que segue, apontar, então, os membros que não realizam modificações de estado (seguras para serem chamadas com const).

class Person {
  int _age {};
public:
  auto age() const { return _age; }       // this method is not allowed to mutate the object.
  auto set_age(int age) { _age = age; }
};

No exemplo acima, a implementação do método age, marcado com o modificador const, cumpre o contrato de não gerar modificações de estado, afinal, apenas retorna um valor.

class Person {
  int _age {};
  int _readcount{};
public:
  auto age() const {
    _readcount++; 
    return _age; 
  }
  auto set_age(int age) { _age = age; }
};

Já o código acima não irá compilar porque o compilador consegue identificar que o “compromisso” assumido no contrato (interface pública da classe/struct) não foi respeitado.

Como o programador expressa “retornos const

Caso o valor retornado seja uma referência, também é possível determinar se ela poderá, ou não, ser modificada.

class Point2 {
  int _x;
  int _y;
public:
  Point2(int x, int y) : _x(x), _y(y) {}
  auto x() const { return _x; }
  auto set_x(int x) { _x = x; }
  auto y() const { return _y; }
  auto set_y(int y) { _y = y; }
};

class Circle {
  Point2 _center;
  double _radius;
public:
  Circle(int centerX, int centerY, double radius) :
    _center(Point2(centerX, centerY)), _radius(radius) {}

  auto& center() const { return _center; };
  auto radius() const { return _radius; };
};

No código acima, por exemplo, o getter center retorna uma referência “constante” para um objeto do tipo Point2, evitando, com toda a segurança, a criação de uma “cópia” na memória.

int main() {
  Circle c(11, 0, 10.0);
  c.center().set_y(10); // will not compile, set_y requires a mutable object.
  return 0;
}

O exemplo acima, não irá compilar porque há uma tentativa de acessar um membro “não-const” em um tipo indicado como “const”.

Eventualmente, o programador desejará que o retorto de um membro de tipo seja “imutável”, porém, sem impor restrições de que a execução do método em si modifique o estado do objeto. Nesses casos, basta mover a palavra-chave const para antes da declaração do tipo de retorno.

const auto& center() { return _center; };

O compilador do C++ não ajuda?

Para cumprir seu compromisso de conceder ao programador o máximo de acesso a recursos, para obtenção de performance ótima, em um tempo em que compiladores dispunham de poucos KB para processar bases gigantescas de código, C++ já permitiu (e por compatibilidade retroativa, ainda permite) a escrita de código “perigoso”. Entretanto, a linguagem amadureceu e hoje oferece artifícios simples para impedir grandes enganos. Um desses artifícios é a ideia de “const correctness“.

Trata-se de uma solução genialmente simples para impedir enganos perigosos, simplesmente permitindo que o programador expresse o que deseja do código de maneira cuidadosa. O compilador do C++ não ajuda “direcionando” o programador para “não enganos”, mas garantindo que suas intenções, expressas no código, sejam concretizadas.

Compartilhe este capítulo:

Compartilhe:

Comentários

Participe da construção deste capítulo deixando seu comentário:

Inscrever-se
Notify of
guest
0 Comentários
Feedbacks interativos
Ver todos os comentários

Elemar Júnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia. 

Desenvolvendo gente que faz a diferença

reconhecida excelência da EximiaCo, em consultorias e assessorias, aplicada no desenvolvimento de competências através de publicações e capacitações abertas e in-company.

TECH

&

BIZ

-   Insights e provocações sobre tecnologia e negócios   -   

55 51 9 9942 0609  |  me@elemarjr.com

55 51 9 9942 0609  |  me@elemarjr.com

bullet-1.png

55 51 9 9942 0609   me@elemarjr.com

0
Quero saber a sua opinião, deixe seu comentáriox
()
x