Programar & Documentar

Programar & Documentar

No âmbito da minha dissertação tenho trabalhado com uma biblioteca open source que, infelizmente, está mal documentada. E eu sou grande apologista de projetos open source, mas deixemos isso para um outro commit.

Tal como as metodologias agile nos ensinaram, existe sempre um trade-off entre working software e documented software. Ainda assim, há cuidados básicos que devemos ter para deixar o código tão legível e claro quanto possível. Neste commit foquemo-nos nas assinaturas das funções.

Imaginemos a função pause, que coloca a thread onde é executada em pausa. Se essa função estiver definida como

void pause(int timeToPause);

existirão, naturalmente, dúvidas sobre qual a unidade de tempo a ser passada como argumento. Segundos? Milisegundos? Minutos?

Apesar de já ser mais aceitável termos algo como void pause(int timeToPauseInSeconds);, porque não declará-la como

void pause(Time timeToPause);

ou com os tipos std::chrono::duration1 introduzidos no C++11, que pretendem terminar com esta ambiguidade:

void pause(milliseconds timeToPause);

Agora imaginemos que queremos aumetar a granularidade das unidades, de forma a conseguir pausar ao nível do nanosegundo. Neste caso, passaríamos a ter

void pause(nanoseconds timeToPause);

Agora imaginem que precisamos de outra função para parar ao nível do microsegundo… Ok, acho que já entenderam onde quero chegar. Obviamente podemos escrever esta função de uma forma mais genérica

template<typename T>
void pause(const T& timeToPause)
{
   std::this_thread::sleep(timeToPause);
}

void action()
{
   // ... outras ações...
   pause(30); // mau: não sabemos as unidades
   pause(milliseconds(30)); // bom
   pause(30ms); // bom: possível a partir do C++14
}

que, já agora, utiliza um template da biblioteca de suporte a threads também introduzida no C++11. Esta combinação permite interoperabilidade com o tipo de granularidade de tempo que deserjarmos.

Reparem como introduzi outra alteração no código que ainda não discutimos. Decidi passar o valor por referência e, como tal, utilizei a keyword const como garantia de que o valor não irá ser alterado. Isto não só me dá segurança de que irei obter um erro em compile-time se a regra for violada, como também serve de boa documentação. De que forma? Sigam o raciocínio com um novo exemplo.

Imaginemos a função countToTen, que recebe um inteiro por referência e conta, a partir desse valor, até 10:

void countToTen(int &number);

Sem vermos a documentação (se existir), ou o código, não temos garantia nenhuma de que o valor de number se mantém inalterado, como na implementação que se segue

void countToTen(int &number)
{
   while(number < 10)
      std::cout << ++number << std::endl;
}

Assim como também podemos ter uma implementação que não muda o valor da variável

void countToTen(int &number)
{
   int n = number;
   while(n < 10)
      std::cout << ++n << std::endl;
}

Independentemente da implementação, a simples inclusão da keyword const não só melhora a qualidade e robustez do código, como também dá garantias a futuros utilizadores da vossa função que o valor variável que passam como argumento não irá ser alterado. Assim, em operações que necessitem de receber um valor por referência, a keyword const é vossa amiga!

void countToTen(const int &number);

E sim, eu sei que a questão, neste caso, fica resolvida se passarmos por valor, mas imaginemos que o gasto computacional para copiar um inteiro é relevante o suficiente para passarmos apenas uma referência.

  1. http://en.cppreference.com/w/cpp/chrono

comments powered by Disqus