Revista Do Linux
 
  
  
 

  Capa
  Evento
  Serviço Público
  Corporativo
  Tutorial ECF Remota
  Tutorial Xdm
  Software
  Entrevista
  Estudo de Caso
  Distro
assinantes
 

Standard Template Library parte I

Esta série de artigos pretende apresentar alguns recursos da Biblioteca padrão do C++, a Standard Template Library (STL), que foi definida por volta de 1997. A STL possui contêineres, rotinas de E/S, strings, suporte a computação numérica e muito mais, e utilizá-la traz muitas vantagens. Sendo parte da Biblioteca Padrão, é encontrada em vários compiladores C++ e IDEs existentes, exatamente como a biblioteca padrão do C

Os artigos são destinados àqueles que já têm um conhecimento prévio de C++, e para entender e utilizar eficientemente a STL também é necessário ter um conhecimento prévio de programação genérica (gabaritos/templates), pois a STL é completamente baseada nessa tecnologia. Conheça a seguir algumas de suas principais vantagens e características.

Portabilidade

Por ter sido padronizada, programas utilizando a STL de maneira correta podem ser compilados em uma vasta gama de compiladores e plataformas, tanto comerciais quanto livres.

Desempenho

Cada implementação da STL deve seguir corretamente as definições sobre o desempenho de cada contêiner, algoritmo ou função. Essas definições de desempenho foram criadas por comitê, como, por exemplo, que o tempo necessário para acessar um elemento randômico em um vetor deve ser constante, independentemente do tamanho do vetor, assim como adicionar um novo elemento ao final do mesmo, mas inserir um elemento ao início de um vetor consome um tempo linearmente proporcional ao número de elementos já contidos nele.

Com essas definições, um programador terá certeza de que utilizar uma certa função em um contêiner vai sempre funcionar da maneira esperada, independente da implementação. Claro que uma implementação pode ser mais rápida que outra, mas deve sempre seguir as definições de desempenho para cada operação, evitando surpresas.

Suporte

Como a STL é padronizada, um programador que utilize uma implementação X da STL pode facilmente dar suporte a um programador da implementação Y, assim, qualquer lista ou fórum pode fornecer suporte a várias implementações. Os newsgroups comp.lang.c++ e comp.lang.c++.moderated são bons lugares para se tirar dúvidas a respeito da STL.

Variedade

Existem inúmeras implementações da STL, tanto comerciais quanto livres, vários compiladores C++ vêm com o suporte à STL incluídos, e é possível utilizar uma implementação de outro fabricante com o seu compilador.

Facilidade de uso

A STL foi criada com a finalidade de ser facilmente utilizada. De nada adiantaria se a biblioteca fosse complicada ou se dependesse de muitas linhas de código para inicializar um vetor. Para se criar um vetor de inteiros e uma string basta declarar:

std::vector int_array;
std::string nome;

O std:: serve para informar ao compilador que o tipo vetor se encontra no namespace std, assim como todas as funções e classes da biblioteca padrão. Caso haja alguma outra biblioteca que tenha definido uma classe vector, a mesma não causará problemas de ambigüidade no programa que utilize essa biblioteca e a biblioteca padrão.

Flexibilidade

Muitas vezes, é necessário criar um vetor ou lista em uma memória específica ou mesmo compartilhada, ou utilizar uma string com caracteres do tipo wchar_t em vez do char. A STL também foi criada com o intuito de, além de ser fácil de ser utilizada, ser fácil de ser extendida ou configurada para usos específicos.

A classe vector, por exemplo, é declarada da seguinte maneira:

template  > class vector

Ao definir um vetor como o int_array acima, está sendo utilizado o alocador padrão da STL para o tipo int, mas pode-se criar uma classe alocadora que funcione com memória compartilhada e assim definir o vetor que utilize memória compartilhada:

class mem_comp_alloc { ... };
std::vector int_array_comp;
As definições entre os caracteres < e > são os parâmetros de gabarito, e definem que a classe vector aceita dois "tipos" como parâmetros, sendo o primeiro do tipo _Tp que pode ser desde um int até qualquer tipo ou classe definidos pelo usuário. O segundo parâmetro é do tipo _Alloc que tem como valor default std::allocator<_Tp>, ou seja, uma classe alocadora que aceita como parâmetro o mesmo tipo que foi informado para o vector.

O tipo _Alloc, embora não seja definido na definição da classe vector, deve ter funções membros, typedefs e propriedades de uma classe allocator, ou o programa não irá compilar, gerando um erro de compilação dentro dos cabeçalhos de inclusão da STL.

Já a classe std::string na verdade é um typedef para std::basic_string, std::allocator>, assim como std::wstring é um typedef para uma classe basic_string do tipo wchar_t.  Ou seja, o gabarito std::basic_string aceita  três parâmetros: um tipo qualquer para armazenamento de caracteres (char, unsigned char, wchar_t, etc.), um char traits, que define as operações sobre o tipo/classe de caracteres informado, e um alocador para o tipo informado. O typedef para std::string cria uma basic_string que utiliza o tipo char como tipo de armazenamento de caractere, com uma classe que define as operações sobre o tipo char, e um alocador para o tipo char, economizando um certo trabalho de digitação.

Portanto, a classe basic_string pode ser parametrizada em termos de tipo de caractere, de char traits, que define as operações sobre um tipo/classe de caracteres e um alocador. Sendo a STL completamente flexível, a gama de utilização para a mesma é infindável.

Contêineres padrões

A STL possui os seguintes contêineres padrões: vector, list, string, deque, set, multiset, map e multimap; e os tipos que não são contêineres.: bitset, valarray, stack, queue e priority_queue.

Algumas implementações também definem os contêineres não-padrão: rope, slist, hash_set, hash_multiset, hash_map e hash_multimap. Se portabilidade for importante, é melhor ficar longe destes contêineres, pois pode haver diferenças entre um hash_map da implementação A e da implementação B.

Escrever sobre a STL pode levar a mais de um livro inteiro O livro The C++ Standard Library: A Tutorial and Reference de Nicolai M. Josuttis, Addison-Wesley e The C++ Programming Language (3ª edição), de Bjorne Stroustrup, dedica mais de quatro capítulos para falar da STL, o restante é sobre a linguagem C++ atualizada. Discutimos neste artigo a classe vector. Para outros artigos serão discutidas as classes string, map, list e etc.

std::vector

Quando é necessário escrever um programa que leia alguns inteiros fornecidos pelo usuário, ordená-los e mostrar ao usuário o resultado, o seguinte programa normalmente é escrito:

#define MAX_NUMEROS 1024;
int nums[MAX_NUMEROS];
int a;
int i=0;

for(;;) {
        if( !(a = lenumero() ) ) break;
        nums[i++] = a;
}
sortnumeros( nums, i-1 );
// provavelmente usando qsort ou algum algoritmo
// criado pelo próprio desenvolvedor

mostranumeros( nums, i-1 );
// algum loop enviando os números para o console

Existem alguns problemas neste programa:

  • As macros devem ser abolidas de um programa C++;
  • O usuário deste programa não deve digitar mais de 1024 números ou o programa explodirá;
  • Todas as funções que trabalham com a matriz nums precisam sempre saber o tamanho utilizado da matriz.

    Este mesmo programa escrito utilizando a STL ficaria mais ou menos desta maneira:

    std::vector nums;
    nums.reserve(512);
    int a;
    
    for(;;) {
            if( !(a = lenumero() ) ) break;
            nums.push_back(a);
    }
    std::sort (nums.begin(), nums.end() );
    mostranumeros( nums );
    // pode utilizar ostream_iterator ou std::for_each
    

    As melhorias:

  • O usuário pode informar qualquer quantidade de valores, pois um vetor pode crescer conforme necessitar de mais espaço;
  • A biblioteca padrão fornece a função sort, que é bem mais eficiente que a função C qsort;
  • As funções que utilizam o vetor como parâmetro não precisam ter outro parâmetro só para saber o tamanho do mesmo, pois a nums.size() retorna este número, assim como existe a função nums.end().

    Por padrão, um vetor reserva inicialmente 10 posições para o tipo utilizado, normalmente dobrando seu limite cada vez que este é excedido. A função reserve, como diz o nome, reserva a quantidade desejada, evitando redimensionamentos.

    A quantidade reservada por um vetor pode ser obtida através da função capacity(). Quando size() == capacity() e mais um valor é adicionado ao vetor, o mesmo executa os seguintes passos:

  • Pede ao alocador memória para conter o dobro da capacidade atual;
  • Copia os elementos da memória antiga para a nova memória alocada;
  • Destrói os objetos contidos na memória antiga;
  • Devolve a memória antiga para o alocador.

    Normalmente, é uma boa idéia reservar o espaço necessário para o vetor quando temos uma estimativa da quantidade de objetos que serão adicionados.

    Outra informação importante é que os valores guardados no vetor são uma cópia do valor enviado a ele através do push_back. Portanto, o tipo ou classe utilizados num vetor devem ter o construtor de cópia e o operador de cópia definidos com acesso público (normalmente o padrão, a menos que definido o contrário), assim como estas funções não devem ser "caras" (gastar muitos recursos da máquina a cada cópia efetuada). Caso seja impossível utilizar o operador e construtor de cópia, ou que seja uma tarefa que consuma muitos recursos, deve-se preferir utilizar um vetor para ponteiros:

    class minha_classe { ... };
    std::vector vec;
    // não esquecer que esses ponteiros
    // não são automaticamente destruídos
    vec.push_back( new minha_classe() );
    

    Algoritmos

    A STL fornece mais de duas centenas de algoritimos para serem utilizados com os contêineres padrão, além das funções membros de cada contêiner.

    Só para funções de ordenação existem as funções sort, partition, nth_element, stable_sort e etc. Estas funções de ordenação são mais rápidas que qualquer outra função de ordenação feita artesanalmente, pois os desenvolvedores da implementação utilizam otimizações que seriam impossíveis de ser utilizadas por desenvolvedores externos, sem contar que essas funções foram muito bem testadas e analisadas por milhares de usuários.

    Além dos algoritimos de ordenação, existem algoritimos para busca (find, find_if, equal_range, binary_search, etc), iteração (for_each, accumulate, transform) e muitas outras. O ideal é que quase todos os loops executados num contêiner sejam trocados por um algoritimo, que executará mais rápido e com menos erros que um loop feito "à mão".

    Para outros tipos de loop, como enviar os inteiros do vetor nums acima à saída padrão, pode-se utilizar a classe ostream_iterator:

    std::copy(nums.begin(), nums.end(), ostream_iterator(cout, "\n"));
    

    Existe até um algoritimo/iterator para capturar os inteiros de uma entrada padrão ou arquivo diretamente para um contêiner.

    Iterators

    Um contêiner do tipo vetor deve fornecer acesso seqüencial e constante aos seus dados, e os dados podem ser acessados através do operador subscrito. Para acessar o primeiro item do vetor num deve-se utilizar nums[0]e, para acessar o último, nums[nums.size()-1]. O valor retornado é uma referência:

    std::vector nums;
    
    // preenche nums com valores
    ...
    //
    
    nums[0] = 1; // escreve no primeiro valor
    *(++(&nums[0])) = 10; // no segundo
    nums[5] = 15; // no quarto valor
    mostranumeros( &nums[0], nums.size()-1 );
    // usa função legada
    

    Rapidamente, um loop para fazer algo num contêiner viria à tona:

    for( int i=0; i
    
    

    Mas este loop acima não é a maneira correta de trabalhar com contêineres.

    Para se "navegar" num contêiner ou, normalmente, para se fazer qualquer tipo de operação, deve-se usar os iterators, classes definidas para emular um ponteiro para um contêiner, mas que contêm diversas funções membros, typedefs, e propriedades para ser possível utilizá-los com entrada e saída de diversos algoritimos e até mesmo de outros contêineres.

    Portanto, o loop acima pode ser reescrito de duas maneiras, sendo a segunda a maneira mais recomendada em termos de eficiência e leitura de código:

    1) for( std::vector::iterator i = nums.begin(),
            i != nums.end(), ++i )
            {
                    *i = qualquerfuncaoqueretornaumint();
            }
    
    2) for_each( nums.begin(), nums.end(),qualquerfuncaoqueretornaumint );
    

    O iterator não é um ponteiro! Embora se pareça com um ponteiro, na verdade, ele é uma classe com os operadores *, &, ++, --, etc definidos. Os algoritimos e funções membros dos contêineres trabalham com os iterators. Por exemplo, para encontrar o primeiro número 15 no vetor nums:

    std::vector::iterator p = find(num.begin(), nums.end(), 15);
    if( p != nums.end() ) *p = 16; // achou! Muda pra 16
    

    O algoritimos find aceita dois iterators como entrada demarcando a região a ser procurada e um valor para ser encontrado, usando begin() e end() está sendo informado ao algoritimos para procurar no contêiner inteiro. Caso seja encontrado é retornado um iterator apontando para o valor encontrado ou end() se não foi encontrado.

    Se, no exemplo acima apenas for necessário encontrar o valor, sem alterá-lo, pode-se utilizar o const_iterator, útil para quando se faz uma busca ou iteração num contêiner dentro de uma classe numa função membro const, ou apenas para certificar-se de que o contêiner não será alterado.

    std::vector::const_iterator p = find(num.begin(), nums.end(), 15);
    if( p != nums.end() ) cout << "Achou!";
    

    Os iterators são classificados ainda com relação ao tipo de acesso que oferecem a um contêiner: forward (acesso unidirecional), bidirecional e randômico. Um contêiner da classe list não oferece iterator randômico, por exemplo.

    Este artigo é apenas uma breve introdução à STL, sendo recomendada a leitura de livros como Bjorne Stroustrup, The C++ programming language (3ª edição); Nicolai M. Josuttis, The C++ Standard Library: A Tutorial and Reference; Scott Meyers, Effective STL, dentre outros. Alguns destes livros são encontrados em português.

    Veja na seção Para saber mais alguns sites interessantes sobre a STL que vale a pena visitar. Para os sistemas que desenvolvo, a STL assim como a Biblioteca Padrão C++ têm sido excepcionalmente úteis e eficientes, tanto na escrita quanto na execução destes softwares. Tornaram-se indispensáveis no dia a dia. Até o próximo artigo!

    Para saber mais:
    www.sgi.com/tech/stl/
    www.stlport.org/
    www.boost.org/


    Paulo Machado Ottani Assis - paulo@coral.srv.br


  • A Revista do Linux é editada pela Conectiva S/A
    Todos os Direitos Reservados.

    Política de Privacidade
    Anuncie na Revista do Linux