Está difícil fazer as pazes com C++

Vocês que lêem meus artigos aqui no Bit Is Myth já devem ter percebido que detesto C++, C#, Java e essas linguagens ditas “orientadas à objetos”. Não que não as entenda ou que não tenha trabalhado com elas, justamente o contrário! Mas, não posso deixar de tentar usá-las: Meu projeto (de estudos) chamado VirtualWorlds chegou a um ponto onde seria mais fácil usar C++ para integrar parsers COLLADA ou adicionar alguma camada de abstração para o OpenGL. Só que continuo topando com problemas básicos. Por exemplo, a STL. STL é o acrônimo de Standard Template Library. Não é propriamente uma library no sentido de ter um shared object disponível com um conjunto de classes. É uma “muvuca” de templates que só é compilado junto com o seu código. E a STL, embora seja uma mão na roda, tem lá suas idiossincrasias… Eis uma: Imagine que você tem uma classe simples, chamada Integer:

class Integer {
private:
  int x;
public:
  Integer() : x(0)                       // Construtor default.
    { std::cout << "Integer()\n"; }
  Integer(const Integer&amp; i) : x(i.x) // Construtor de cópia.
    { std::cout << "Copy of Integer(" << i.x << ")\n"; }
  Integer(int i) : x(i)                  // Construtor de conversão.
    { std::cout << "Conv Integer(" << i << ")\n"; } 
  virtual ~Integer()                     // Destrutor.
    { std::cout << "Destructor\n"; } 
  Integer&amp; operator=(const Integer& v) // Operador de assinalamento.
    { x = v.x; std::cout << "operator=()\n"; return *this; } 
  int get() { return x; } // Obtem o membro privado 'x'.
};

Estou criando esse exemplo simplesmente para que, ao usar o gabarito vector da STL possamos ver o que ele faz com a classe…

Neste exemplo temos 3 construtores aqui: Um “default” (que não usaremos), um de cópia (usado quando C++ quer copiar uma instância do objeto) e um conversor de conversão (quando queremos converter um tipo para o “tipo” do objeto da classe – no nosso caso, de ‘int’ para ‘Integer’). E toda a literatura sobre C++ te dirá que é interessante definir um operador de assinalamento “default” (que também não vamos usar aqui). Agora, imagine que você precise de um array com objetos dessa classe. O jeito mais simples é usar um container vector, da STL e, para inserir instâncias da classe Integer no array, basta usar a função push_back():

int main()
{
  // Apenas declaração de vector&lt;Integer&gt; e de um iterator.
  std::vector<Integer> arrayOfIntegers;
  std::vector<Integer>::iterator i;

  // Coloca valores SEMPRE no final do array.
  std::cout << "push 1\n";
  arrayOfIntegers.push_back(1);
  std::cout << "push 2\n";
  arrayOfIntegers.push_back(2);
  std::cout << "push 3\n";
  arrayOfIntegers.push_back(3);

  // Mostra os itens no array.
  std::cout << "\nShow array\n";
  int j = 0;
  for (i = arrayOfIntegers.begin(); i != arrayOfIntegers.end(); i++)
    std::cout << "v[" << j++ << "] = " << i->get() << '\n';

  // Limpa o array.
  std::cout << "\nclear vector\n";
  arrayOfIntegers.clear();

  std::cout << "\nexit.\n";
  return 0;
}

Se você tirar os ‘cout’ do código acima, ele fica bem compacto… Olha só o que acontece na implementação que acompanha o G++ 4.7:

$ g++ -o integer integer.cc
$ ./integer
vector and iterator declaration
push 1
Conv Integer(1)
Copy of Integer(1)
Destructor
push 2
Conv Integer(2)
Copy of Integer(2)
Copy of Integer(1)
Destructor
Destructor
push 3
Conv Integer(3)
Copy of Integer(3)
Copy of Integer(1)
Copy of Integer(2)
Destructor
Destructor
Destructor

Show array
v[0] = 1
v[1] = 2
v[2] = 3

clear vector
Destructor
Destructor
Destructor

exit.

O detalhe esquisito está no resultado marcado em vermelho… Repare que a cada vez que push_back() é chamada  e essa função coloca um novo item no final do array − parece que o novo item é colocado no início do “vetor” e os itens que já estavam nele são copiados, na ordem, para o antes deste novo item! Isso pode ser inferido no caso da inserção do valor 3: Integer(3) é copiado para v e, logo depois, os Integer(1)Integer(2) são copiados também, nessa ordem (depois das cópias feitas, os objetos anônimos originais são destruídos – note que um construtor de conversão é chamado para criar esses objetos anônimos com base na classe Integer).

Será mais lógico simplesmente criar uma cópia no final do array, mas não é isso que a STL faz! Ainda, as instâncias cujas cópias foram feitas desnecessariamente devem ser “jogadas no lixo” e, por isso, temos tantos destrutores chamados quanto itens inseridos anteriormente no “vetor” (a chamada adicional a um destrutor vêm do fato de que o objeto usado na chamada para push_back() precisa também ser liberado!). Ou seja… Para colocar 3 inteiros num array temos 9 objetos sendo criados e 6 sendo destruídos, quando poderíamos ter apenas 3 objetos criados (ou, no máximo 6, se concedermos a destruição do objeto temporário usado no parâmetro de push_back()). Imagine o excesso de operações de criação e destruição se tivessemos lidando com milhares de objetos e esses objetos tivessem conteúdo complexo (como uma coordenada no espaço tridimensional, por exemplo)! Pode ser que push_front(), que coloca os itens no início do array, seja mais performático… mas eu não quero uma “fila” (queue), quero um simples array! É por isso que prefiro fazer algum assim, em puro e simples C:

int *arrayOfIntegers = NULL;
size_t numOfIntegers = 0;

static void *GrowArray(void **pp, size_t size)
{
  void *p;

  // Ok... você poderia fazer isso em C++ também, mas
  // tenha em mente que realloc() é uma função da libc,
  // não da libstdc++!
  if ((p = realloc(*pp, size)) != NULL)
    *pp = p;

  return p;
}

int PushInteger(int x)
{
  int *p;

  if ((p = GrowArray(arrayOfIntegers, 
             sizeof(int) * (numOfIntegers + 1))) == NULL)
    return -1;

  *(p += numOfIntegers++) = x;

  return 0;
}

A diferença deste PushInteger() com o “vector” da STL é que a última pré-aloca um espaço maior e a minha rotina aloca sob demanda. Mas a minha adiciona um item ao final do vetor sem copiar todo o resto para o início, que é mais desastroso do ponto de vista da performance! Antes que você pergunte: Para retirar o primeiro e o último itens do vetor, e para apagar os itens do array, basta fazer algom assim:

int PopFirst(int *x)
{
  if (numOfIntegers < 1)
    return -1;

  *x = arrayOfIntegers[0];

  // Move um BLOCO de memória... Não é a mesma coisa que
  // chamar construtores de cópia e destrutores para cada entrada
  // do array!
  memcpy(arrayOfIntegers, arrayOfIntegers + 1, 
           sizeof(int) * --numOfIntegers);

  arrayOfIntegers = GrowArray(arrayOfIntegers, 
                      sizeof(int) * numOfIntegers);

  return 0;
}

int PopLast(int *x)
{
  if (numOfIntegers < 1)
    return -1;

  *x = arrayOfIntegres[--numOfIntegers];
  arrayOfIntegers = GrowArray(arrayOfIntegers, sizeof(int) * numOfIntegers);

  return 0;
}

void ClearArray(void)
{
  free(arrayOfIntegers); 
  arrayOfIntegers = NULL;
  numOfIntegers = 0;
}

Note que as funções PushInteger() e Pop*() retornam 0 em caso de sucesso ou -1 em caso de falha, evitando totalmente o uso de blocos try..catch do C++. Mas, estou sendo chato, não? Afinal, deve existir alguma maneira de obter a mesma performance em C++! E, de fato, há… você pode criar um “vector” de ponteiros:

std::vector<Integer *> arrayOfIntegers;

Sempre que adicionarmos algum ponteiro para um objeto, via push_back(), aquelas cópias continuarão a ser feitas, mas de ponteiros, não dos objetos em si… Ainda teremos problemas de performance e um efeito colateral: Ao chamar clear(), você não estará destruindo os objetos! Estará somente liberando o espaço usado pelo “vector”. Se precisar destruir os objetos apontados deverá executar “delete” para cada item do array! Mas, mas… ora… Se você usa classes e a STL justamente para evitar a notação “complicada” dos ponteiros, então pra quê usar C++?

UPDATE:

Me foi chamada a atenção para o fato de que o template vector não pré-aloca coisa alguma se o construtor default for chamado. Por esse motivo o container “move” os itens já inseridos via push_back (o que considero um ERRO de implementação). De fato, se reservarmos espaço antes de “empurrar” o primeiro inteiro:

...
  // Apenas declaração de vector&lt;Integer&gt; e de um iterator.
  std::vector<Integer> arrayOfIntegers;
  std::vector<Integer>::iterator i;

  // Reserva espaço para 3 inteiros.
  arrayOfIntegers.reserve(3);
  
  // Coloca valores SEMPRE no final do array.
  std::cout << "push 1\n";
...

O resultado será um pouco diferente: A cópia do objeto continuará sendo feita (o que é natural), mas nenhuma movimentação será…

Mesmo assim, a implementação em C ainda é mais rápida e eficiente!

Anúncios

2 comentários sobre “Está difícil fazer as pazes com C++

  1. 1) não é assim que se usa um vector, apesar do vector possuir a possibilidade de crescer conforme chega a seu limite, isso não deve ser feito a menos que seja necessário.
    se vc vai criar um vector com 3 posições, aloque inicialmente as 3 posições.
    se você sabe oq vai colocar no vector utilize list_initializers vector{1, 2, 3}; (padrão c++ 11 ou c++0x).
    2) para o problema que você está descrevendo, vector não é a melhor estrutura, vc busca uma lista ligada ou uma double endeded queue (deque)
    3) realloc é do mal, cada vez que se usa realloc, uma abelha morre, e é por isso que as abelhas estão sumindo. é muito mais barato alocar mais memória e copiar que fazer um realloc, em 15 anos de C só vi realloc ser utilizado da maneira correta 2x pelos, e pelos caras do BSD.

    4) quanto a ponteiros, c++ te incentiva sim a usar ponteiros, mas c++ moderno te incentiva a nunca deletar nada na mão, você utiliza 3 templates específicos para isto: std::shared_pointer que faz contagem de referencia, este primeiro encapsula seu ponteiro dentro de uma variável de pilha que é deletada no fim da execução, criando uma espécie de pointer_pool para você deletando o conteúdo do ponteiro após as referencias serem destruídas.
    std::unique_ptr é utilizado para quando vc precisa instanciar um ponteiro que irá fatalmente ser destruído no fim da execução da função, associando o mesmo ponteiro a uma variável simples de pilha, este é interessante por questões de que se sua função lança uma excessão, ou tem muitos pontos de retorno, a variável vai ser desalocada após a função terminar, não importa como esta termine, evitando vazamento de memória
    e o weak_ptr que faz contagem de referencia circular baseado em grafos, apesar do weak_ptr ser um pouco mais lento que os outros 2, resolve problemas de referencia circular.

    para uso de vector, é necessário seguir a seguinte estratégia: aloque o vector quando este é iniciado com MAIS ou menos a quantidade de ítens que vc quer usar, se isto os ítens estão acabando, DOBRE o buffer do mesmo.
    caso você jamais vai usar resize na vida, o melhor para você é usar array, que é basicamente como os old fashioneds arrays em c,
    com a diferença de que os templates os tornam seguros contra buffer overflow,
    array(size); é o mesmo que T[size] = {0}; em c
    mas os templates te dão todo tipo de segurança possível sobre o tratamento de tipos, se o array explodir, o próprio array te diz que explodiu, em vez de ter comportamento não previsto como em c.

    mas quero relembrar: NUNCA use realloc, a menos que você saiba exatamente o motivo pelo qual você quer usar realloc, seja em c, seja em c++,

    é mais barato alocar, copiar e remover 10 buffers que fazer um único realoc em média, se vc tiver memória livre adjacente, pode até que seja rápido, mas isto não é uma métrica

    1. Stranjo, o template vector deveria ser o equivalente a um array que se auto-ajusta, em tamanho e ele é diferente de queue no sentido em que deveria ser o mais eficiente possível ao colocar itens no final. Ainda, vector pré aloca um espaço considerável (que pode ser modificado via função membro reserve()). O problema que tenho é que eu PRECISO de um array, não de uma lista encadeada. O container vector é muito usado, pela turma do C++, para manter um buffer de vetores, por exemplo, para usá-lo em funções como glBufferData, do OpenGL.

      A função realloc é usada até mesmo pela biblioteca padrão… Dê uma olhada no código fonte da libc. E, quanto as abelhas… well… fuck’em! hehe.

      Outra coisa, auto_ptr’s são condenados até por membros da comissão de padronização desde a versão 3 da especificação (pré TR1).

      O container array não expande de acordo com a necessidade!

      Fico ainda com as funções de alocação de memória do C… Eu posso pré-alocar por minha própria conta se quiser e jamais ficar fazendo cópias desnecessárias de um lado para outro.

      Note que escrevo com interesse em demonstrar PERFORMANCE, não “segurança” ou “facilidade de codificação”… Mesmo quanto à “segurança”, como acredito ter demonstrado em outros artigos, a STL deixa a desejar… A norma é “não existem erros”, como na especificação do operador “new” que diz que ele SEMPRE retornará um ponteiro para o free store… É mesmo? Mesmo quando não há mais “free” store? Quando é que se resolve testar por problemas de alocação na STL? Com realloc(), pelo menos, testo sempre que realoco memória…

      Não me entenda mal, acho a STL muito interessante, para projetos que não exijam performance e onde ficar copiando instâncias pra lá e pra cá não seja um problma… Isso só não se aplica a meus projetos.

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s