Clearly, I reject the view that there is one way that is right for everyone and for every problem.
Bjarne Stroustrup

Em C++, uma classe deve ser responsável por gerenciar completa e eficientemente os recursos que referencia.

Memória, threads, conexões com o banco de dados, mutexes, são apenas alguns exemplos de recursos, potencialmente escassos, que, uma vez alocados, se não forem adequadamente liberados, podem levar o ambiente produtivo a exaustão. Pior ainda, se forem liberados “antes do tempo”, podem conduzir o programa a um comportamento inválido.

Nos últimos anos, a gestão da memória, como recurso potencialmente escasso, em C++, ficou muito mais simples com o advento dos smart pointers. Entretanto, o desafio ainda persiste para todos os demais tipos de recursos que demandam alocação/liberação.
0
Considerações?x

C++, no lugar de ser “opinativa” (como outras linguagens) quanto a forma que recursos escassos devem ser gerenciados (adquiridos, mantidos e descartados), opta por dar controle total ao desenvolvedor. Essa decisão certamente desagrada alguns e agrada muitos.
0
Consideraçõesx

Como prática eficiente, nos últimos anos, convencionou-se que classes que precisam lidar com recursos potencialmente escassos devem implementar comportamentos especiais:

  1. Destruição – liberando recursos sob “gerência” do objeto;
  2. Construção com cópia– evitando compartilhamento de controle sobre “recursos escassos” em objetos criados “copiando” um objeto existente;
  3. Atribuição com cópia evitando compartilhamento de controle sobre “recursos escassos” em objetos com valores copiados de  um objeto existente;
  4. Construção com transferência– evitando alocações desnecessárias de recursos escassos, quando um objeto novo estiver sendo criado “copiando” dados de outro, pré-existente, que não será mais utilizado;
  5. Atribuição com transferência – evitando alocações desnecessárias de recursos escassos, quando um objeto estiver “copiando” dados de outro, pré-existente, que não será mais utilizado;

Destruição

Se pegar emprestado, devolva, meu filho.

minha mãe

Comecemos com uma regra bem simples: se um objeto alocar qualquer recurso potencialmente escasso, deverá garantir sua liberação.

#include <initializer_list>
#include <iostream>

class Buffer {
public:
  Buffer(const std::initializer_list<float>& values)
          : _size{values.size()} {
    _data_ptr = new float[values.size()];
    std::copy(values.begin(), values.end(), _data_ptr);
  }

  auto begin() const { return _data_ptr; }
  auto end() const { return _data_ptr + _size; };

  ~Buffer() {
    delete [] _data_ptr;
    _data_ptr = nullptr;
  }

private:
  size_t _size{0};
  float* _data_ptr{nullptr};
};

int main() {
  auto data = Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f}); 

  for (auto elem : data) {
    std::cout << elem << std::endl;
  }
}

No exemplo acima, memória, um recurso escasso, é alocada pelo construtor e liberada pelo destrutor.

Construção com cópia

Não estrague o brinquedo do amiguinho.

minha mãe

A classe Buffer, implementada no exemplo acima, parece estar fazendo “tudo certo” – ela “aloca” um determinado espaço na memória em sua inicialização, no método construtor, e “libera” memória, mais tarde, ao sair de contexto, no método destrutor. Entretanto, este código não está protegido contra cópias.

int main() {
  auto data = Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f});

  {
    auto copy = data;
  }

  for (auto elem : data) {
    std::cout << elem << std::endl;
    // FAIL!
  }
}

No código acima, a variável copy recebe uma “cópia” do objeto apontado em data. Afinal, o programador fez a opção por realizar a alocação na stack. Assim, todas as referências presentes no objeto data são “replicadas” no objeto copy e, infelizmente, serão destruídas assim que o contexto de copy for encerrado – gerando comportamento inválido.

#include <initializer_list>
#include <iostream>

class Buffer {
public:
  Buffer(const std::initializer_list<float>& values)
          : _size{values.size()} {
    _data_ptr = new float[values.size()];
    std::copy(values.begin(), values.end(), _data_ptr);
  }

  // copy constructor
  Buffer(const Buffer& other) : _size{other._size} {
    _data_ptr = new float[_size];
    std::copy(other._data_ptr, other._data_ptr + _size, _data_ptr);
  }

  auto begin() const { return _data_ptr; }
  auto end() const { return _data_ptr + _size; };

  ~Buffer() {
    delete [] _data_ptr;
    _data_ptr = nullptr;
  }

private:
  size_t _size{0};
  float* _data_ptr{nullptr};
};

A saída, como é indicado no código acima,  é implementar um construtor de cópia que “duplica” as referências protegendo-as.

Eventualmente, outra alternativa seria “deletar” o construtor de cópia tornando tal comportamento a cópia não autorizada.

Buffer::Buffer(const Buffer& other) = delete;

int main() {
  auto data = Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f});

  {
    // No copies!
    auto copy = data;
  }

  for (auto elem : data) {
    std::cout << elem << std::endl;
  }
}

Atribuição com cópia

C++ é tremendamente eficiente em “poupar memória”, mas, as vezes, essa “obsessão” da linguagem acaba se revertendo em dor de cabeça.

A implementação da classe Buffer, com um construtor de cópia, ainda resulta em comportamento indesejado no código que segue:

int main() {
  auto data = Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f});

  {
    auto copy = data;
    data = copy;
  }

  for (auto elem : data) {
    std::cout << elem << std::endl;
    // FAIL
  }
}

No exemplo, ao reatribuir valor a variável data, no lugar de ser criada uma nova instância de Buffer, C++, por padrão, copia os valores de copy para data. Obviamente, o array referenciado em copy é destruído gerando comportamento potencialmente inesperado.

A saída é gerar uma sobrecarga do operador de atribuição.

#include <initializer_list>
#include <iostream>

class Buffer {
public:
  Buffer(const std::initializer_list<float>& values)
          : _size{values.size()} {
    _data_ptr = new float[values.size()];
    std::copy(values.begin(), values.end(), _data_ptr);
  }

  // copy constructor
  Buffer(const Buffer& other) : _size{other._size} {
    _data_ptr = new float[_size];
    std::copy(other._data_ptr, other._data_ptr + _size, _data_ptr);
  }

  // copy assignment
  auto& operator=(const Buffer& other) {
    delete [] _data_ptr;
    _data_ptr = new float[other._size];
    _size = other._size;
    std::copy(other._data_ptr, other._data_ptr + _size, _data_ptr);
    return *this;
  }

  auto begin() const { return _data_ptr; }
  auto end() const { return _data_ptr + _size; };

  ~Buffer() {
    delete [] _data_ptr;
    _data_ptr = nullptr;
  }

private:
  size_t _size{0};
  float* _data_ptr{nullptr};
};

Outra alternativa seria impedir atribuições.

auto& operator=(const Buffer& other) = delete;

Construção e atribuição com transferência

Qual seria o resultado lógico do código abaixo?

void print(Buffer data) {
  for (auto elem : data) {
    std::cout << elem  < < std::endl;
  }    
}

int main() {
  print(Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f}));
  return 0;
}

Por padrão, o comportamento seria a criação de um objeto na stack associado a função mainque seria, imediatamente, copiado para um novo objeto na stack associado a função print.

Felizmente, quase todos os compiladores modernos reconhecem tal desperdício e “evitam” a cópia.

Use referências sempre que possível

O código a seguir possui só méritos. A função print recebe uma referência somente leitura para um objeto.

void print(const Buffer& data) {
  for (auto elem : data) {
    std::cout << elem << std::endl;
  }    
}

int main() {
  auto data = Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f});
  print(data);
  return 0;
}

Sempre que possível utilize referências evitando cópias desnecessárias.

Eventualmente, entretanto, o compilador não conseguira detectar tais desperdícios, competindo ao programador fazer a gestão.

void print(Buffer data) {
  for (auto elem : data) {
    std::cout << elem << std::endl;
  }    
}

int main() {
  auto data = Buffer({1.0f, 2.0f, 3.0f, 4.0f, 5.0f});
  print(std::move(data));
  return 0;
}

No exemplo, utilizou-se std::move como forma a indicar que o que se deseja fazer é uma transferência de recursos escassos. Tal função, utiliza-se de implementações de construção e movimentação por transferência para evitar desperdícios.

#include <initializer_list>
#include <iostream>

class Buffer {
public:
  Buffer(const std::initializer_list<float>& values)
          : _size{values.size()} {
    _data_ptr = new float[values.size()];
    std::copy(values.begin(), values.end(), _data_ptr);
  }

  // copy constructor
  Buffer(const Buffer& other) : _size{other._size} {
    _data_ptr = new float[_size];
    std::copy(other._data_ptr, other._data_ptr + _size, _data_ptr);
  }

  // move constructor
  Buffer(Buffer&& other) : _size{other._size}, _data_ptr{other._data_ptr} {
    other._data_ptr = nullptr;
  }
  
  // copy assignment
  auto& operator=(const Buffer& other) {
    delete [] _data_ptr;
    _data_ptr = new float[other._size];
    _size = other._size;
    std::copy(other._data_ptr, other._data_ptr + _size, _data_ptr);
    return *this;
  }
  
  // move assignment
  auto& operator=(Buffer&& other) {
    std::cout << "move copy";
    _size = other._size;
    _data_ptr = other._data_ptr;
    other._data_ptr = nullptr;
    return *this;
  }
  
  auto begin() const { return _data_ptr; }
  auto end() const { return _data_ptr + _size; };

  ~Buffer() {
    delete [] _data_ptr;
    _data_ptr = nullptr;
  }

private:
  size_t _size{0};
  float* _data_ptr{nullptr};
};

Bem além da memória…

Todos os exemplos desse apêndice mostraram a aplicação da “regra dos cinco” em uma classe que gerenciava a memória (um exemplo de recurso escasso). Entretanto, de todos os recursos, memória é, potencialmente, o mais fácil de gerenciar.

As coisas podem ficar realmente desafiadoras na gestão de recursos como threads que não podem ser simplesmente “duplicadas” ou transferidas. Daí a importância de entender melhor cada cenário dar ao programador “poder para decidir”.

O compilador pode e deve ajudar, sempre que possível, a evitar enganos. Mas, nem só com receitas prontas são feitos programas, principalmente em systems programming.

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