Feeds:
Posts
Comentários

Então, quais são as opções para lermos um arquivo inteiro para memória (armazenando-o em uma std::string, por exemplo) em C++? O programa abaixo ilustra algumas alternativas simples.

#include <iostream>
#include <fstream>
#include <sstream>
#include <iterator>
#include <string>

void stringstream_read_file();
void istream_iterator_read_file();
void istreambuf_iterator_read_file();

int main()
{
  //Lê copiando o buffer file stream para o string stream.
  stringstream_read_file();

  //Lê iterando pelo stream e fazendo extração com operator>>.
  istream_iterator_read_file();

  //Lê iterando diretamente pelo buffer e copiando caracteres.
  istreambuf_iterator_read_file();

  return 0;
}

void stringstream_read_file()
{
  std::ifstream fs("file.txt");
  if (!fs.good())
    std::cout << "Error." << std::endl;
  else
  {
    std::ostringstream ss;
    ss << fs.rdbuf();
    std::cout << ss.str() << std::endl;
  }
}

void istream_iterator_read_file()
{
  std::ifstream fs("file.txt");
  if (!fs.good())
    std::cout << "Error." << std::endl;
  else
  {
    fs.unsetf(std::ios_base::skipws); //Para manter os espaços.
    std::string s((std::istream_iterator<char>(fs)), std::istream_iterator<char>());
    std::cout << s << std::endl;
  }
}

void istreambuf_iterator_read_file()
{
  std::ifstream fs("file.txt");
  if (!fs.good())
    std::cout << "Error." << std::endl;
  else
  {
    std::string s((std::istreambuf_iterator<char>(fs)), std::istreambuf_iterator<char>());
    std::cout << s << std::endl;
  }
}

Outras idéias?

Vários programadores C++ devem concordar que a interface do template std::basic_string [1] não é das mais ricas. Provavelmente, muitos já até criaram suas próprias funções utilitárias para lidar com strings. Eu resolvi publicar as minhas.

A pequena coleção de algoritmos em string não é tão poderosa e completa como a Boost String Algorithms Library. Também não é tão genérica quanto a da Boost. Isso, por que eu assumo que todos os tipos devem ser instâncias do template std::basic_string. Mesmo assim, ainda podem ter situações que compensem seu uso.

  • Se você está trabalhando com ASCII [2], há versões das funções que aceitam qualquer tipo que seja uma instância do template std::basic_string parametrizado com char e std::char_traits<char> como sendo os dois primeiros argumentos template (o alocador não importa). Essas versões não utilizam os mecanismos de localização/internacionalização de C++, mas apenas uma tabela de conversões. Normalmente, isso permite um processamento mais rápido, já que não são realizadas chamadas de métodos virtuais das classes de facets [3]. Naturalmente, para outros encodings existem versões que aceitam um std::locale.
  • Caso não esteja usando a Boost em sua aplicação, pode ser que não queira adicioná-la apenas por necessitar de algumas funções utilitárias para strings.

Bom, uma pequena introdução sobre minha Basic basic_string String Utils (que na verdade é uma versão em inglês deste texto) e o download estão disponíveis em meu site.

[1] Para os que ainda não sabem, ambos os tipos std::string e std::wstring são instâncias do template std::basic_string.

[2] Me refiro ao ASCII convencional de 7 bits, e não extensões como ISO-8859-x.

[3] O mecanismo de internacionalização em C++ (locales) é constituído por uma coleção de templates e classes especiais chamadas de facets.

Um dos fóruns de desenvolvimento C++ que mais gosto é o Codeguru. De vez em quando participo de umas discussões por lá. Semana passada, por exemplo, surgiu um assunto que costuma gerar certas dúvidas. Principalmente, para quem não é muito por dentro do design da STL.

  • Quais os requisitos que tornam uma classe de iteração (um iterator) compatível com os algoritmos da STL?
  • Quais devem ser os cuidados ao implementar um algoritmo para que ele seja compatível com os iteradores da STL?

Bom, as respostas são relativamente simples. Nada mirabolante. Explicações breves seguem abaixo. Mas se alguém quiser ler a discussão original do Codeguru para entender melhor o contexto, os links são este e este.

Vamos para a primeira pergunta. Talvez você tenha criado uma nova estrutura de dados e esteja implementando um iterador para ela. Ou talvez tenha apenas inventado um mecanismo de iteração específico. Por qualquer que seja o motivo, se você tem uma classe de iteração e precisa fazê-la funcionar na STL é necessário seguir algumas diretrizes.

Como sabemos, os iteradores da STL são responsáveis por fazer a “cola” entre os contêineres e os algoritmos. Logo, a definição dos tipos de dados sobre os quais as operações são realizadas é de responsabilidade do iterador. A STL se apoia em um template, std::iterator_traits, para encapsular essa informação. Sua definição é a seguinte:

template<class iterator_t>
struct iterator_traits
{
  typedef typename iterator_t::difference_type difference_type;
  typedef typename iterator_t::value_type value_type;
  typedef typename iterator_t::pointer pointer;
  typedef typename iterator_t::reference reference;
  typedef typename iterator_t::iterator_category iterator_category;
};

O template std::iterator_traits deve possuir uma instanciação válida para todas as classes que representam iteradores, pois os algoritmos dependem dos tipos inferidos para que possam funcionar corretamente. Existem duas maneiras de fazer isso. Uma delas (normalmente recomendada) é definir os tipos acima através de typedefs dentro da própria classe.

class smart_iterator
{
public:
  typedef something value_type;
  typedef something * pointer;
  //...
};

A maneira alternativa é especializar o template std::iterator_traits e lá fazer a definição dos tipos. Essa é exatamente a abordagem que o próprio padrão C++ adota para que ponteiros comuns funcionem como iteradores. Portanto, a especialização de std::iterator_traits para ponteiros já vem de fábrica.

//Especialização para ponteiros (existe uma similar para ponteiros de constantes).
template<class T>
struct iterator_traits<T*>
{
  typedef ptrdiff_t difference_type;
  typedef T value_type;
  typedef T* pointer;
  typedef T& reference;
  typedef random_access_iterator_tag iterator_category;
};

//Especialização para sua classe.
template <>
struct iterator_traits<smart_iterator>
{
  typedef something value_type;
  typedef something * pointer;
  //...
};

Ainda não acabou. Tendo estabelecido os tipos, você precisa se certificar de que a classe de iteração satisfaz as expressões válidas do conceito de iterador que se deseja modelar. Todos os iteradores devem suportar, por exemplo, o operador de dereferenciação (operator*) e o operador de acesso a membro (operator->). Mas alguns conceitos como Bidirectional Iterator ou Random Access Iterator adicionam novos requisitos.

A resposta para a segunda pergunta é bem intuitiva tendo-se em vista os comentários acima. Quando quiser implementar um algoritmo no estilo STL, lembre-se de dois pontos importantes:

  • Algoritmos operam através de iteradores. Acesso aos tipos de dados devem ser sempre feitos com o auxílio de std::iterator_traits. Nunca acesse o tipo diretamente através do parâmetro template correspondente ao iterador.
  • template <class input_iterator_t>
    void cool_algorithm(input_iterator first, input_iterator last)
    {
      //Não faça assim!
      typename input_iterator_t::value_type a; 
    
      //Faça assim!
      typename std::iterator_traits<input_iterator_t>::value_type b;
    
      //...
    }
    
  • Deixe claro na documentação qual o conceito esperado que o iterador modele. Obviamente, utilize apenas as expressões desse conceito (ou de conceitos que ele refina). Não utilize, por exemplo, a expressão --i (assumindo que i seja um iterador) em uma implementação que espera um Forward Iterator, já que ela só é válida para um Bidirectional Iterator.

Sugestão de implementação do construtor de cópia e operador de atribuição em C++:

  • Crie um construtor de cópia com passagem de parâmetro por referência para uma constante.
  • Crie um operador de atribuição com passagem de parâmetro por valor e retorno por referência.
  • Crie uma função membro de swap. Idealmente, que não lance exceções.
  • Dentro do operador de atribuição, faça a troca (swap) entre o argumento construído por cópia e o objeto apontado por this.

Essa é uma solução elegante e sem redundâncias (considerando que todos os swaps estejam corretamente implementados). Além disso, não depende da verificação de auto-atribuição no operator=. Para detalhes relacionados a exception-safety, clique aqui.

class person
{
public:
    person(std::string name);
    person(person const& other); //Construtor cópia.
    person & operator=(person other); //Operador atribuição.

    void swap(person & other) throw();

private:
    std::string name_;
    unsigned short age_;
    /* ... */
};

person::person(std::string name):
  name_(name), age_(0) /* ... */
{}

person::person(person const& other):
  name_(other.name_), age_(other.age_) /* ... */
{}

person & person::operator=(person other)
{
    other.swap(*this); //Troca com a cópia recebida.
    return *this;
}

void person::swap(person & other) throw()
{
    name_.swap(other.name_);
    std::swap(age_, other.age_);
    /* ... */
}

Lembre que nem sempre é necessário declarar/definir esses membros para todas as classes. O código acima visa apenas ilustrar uma forma de implementação.

Leandro T. C. Melo

Alocação de memória é assunto importante em C++. No entanto, nem todo mundo sabe exatamente como as coisas acontecem. Quantos de vocês já não viram códigos carregados de new/delete em situações nas quais objetos de pilha seriam suficientes ou mais indicados?

O livro Exceptional C++ (Herb Sutter) tem vários tópicos sobre gerenciamento de memória. Inclusive, com uma tabelinha bem bacana que descreve as áreas de memória de um programa C++. O detalhe, talvez um pouco polêmico, é que é feita uma distinção entre o heap e o free store (espaço livre para armazenamento). Tentarei explicar.

Creio que todos sabem que a alocação dinâmica é mais cara que na pilha. Ou seja, o uso do new e delete implica em solicitação e devolução de memória ao sistema operacional durante execução do programa. A consequência disso é um preço, em termos de eficiência, maior do que aquele de um objeto criado por valor na pilha. Obviamente, o new é necessário em inúmeros casos. Mas de onde vem essa memória?

Provavelmente, alguns respoderam heap. Outros responderam free store. Há ainda os que dizem que ambos são exatamente a mesma coisa. Então, existe um veredito?

O padrão C++ não utiliza a palavra heap em nenhum momento com relação à memória. Esse termo só aparece no contexto da STL com a conotação da conhecida estrutura de dados. No que diz respeito a alocação dinâmica, está presente apenas o conceito de free store. É de lá que vem a memória dinâmica de um new. Do ponto de vista prático, porém, o free store normalmente corresponde justamente ao que é chamado de heap nos sistemas. Portanto, apesar de certa confusão (ou mal entendido), considero que a forma oficial de se refereir a origem da memória obtida por new seja através de free store. Mas também considero totalmente aceitável que o heap e o free store sejam, de fato, o mesmo lugar.

Uma das origens dessa confusão é que Herb Sutter menciona no livro que o free store é a área de memória dinâmica utilizada por new/delete. Enquanto que o heap é área de memória utilizado pelas funções malloc/free. Não tenho o padrão de C em minhas mãos, mas realmente gostaria de saber se ele utiliza a palavra heap com o significado de área de memória. Por acaso, alguém sabe?

De qualquer maneira, há uma mensagem bem esclarecedora na Intenet em que Bjarne Stroustrup tenta explicar por que foi feita a distinção entre heap e free store por Herb Sutter. De acordo com ele, a idéia era simplesmente deixar claro que uma alocação feita por malloc é diferente de uma alocação feita por new, o que faz sentido total.

Uau… Depois de toda essa discussão deixo um breve resumo da tabela de Exceptional C++ que descreve as 5 áreas de memória de um programa C++. A tabela detalhada também pode ser encontrada em GotW.

const data – Armazena literais de strings e outros valores read-only conhecidos em tempo de compilação, os quais duram durante toda a execução do programa.

pilha – Área de variáveis automáticas.

free store – Memória dinâmica alocada/liberada por new/delete.

heap – Memória dinâmica alocada/liberada por malloc/free.

global/static – Armazena dados globais e estáticos, os quais são inicializados quando o programa começa a executar.

Bom, espero ter sido útil.

Leandro T. C. Melo

Este post é um pouco diferente. Ao invés de escrever sobre o assunto aqui, simplesmente encaminho o link para um artigo no CodeProject. O texto relata os meus passos e aprendizado na implementação do teste de unicidade de URLs (URL-seen test) em um web crawler. Especificamente, o foco é na técnica conhecida como DRUM (Disk Repository with Update Management), utilizada para armazenamento de pares chave/valor e verificações assíncronas. Se você tem interesse no tópico e não está com muita preguiça (ou muito trabalho) neste momento, boa leitura! Ah… é claro. Sugestões são sempre bem-vindas.

Leandro T. C. Melo

Provavelmente, muitos programadores C++ já se depararam com certas dúvidas relacionadas ao operador sizeof. Se ainda não aconteceu com você, quem sabe este post evite potenciais dores de cabeça. Mas mesmo se tiver um histórico sobre o assunto, ainda há chances de descobrir uma curiosidade.

Considerando os tipos fundamentais de C++, algumas perguntas comuns que aparecem por aí são as seguintes:

    - Qual o resultado de sizeof(int)?
    - sizeof(long) é maior do que sizeof(int)?
    - É garantido que sizeof(char) seja 1 (um)?

Apesar de estar particularmente interessado na última delas, aproveito a ocasião para esclarecer (ou relembrar) as duas primeiras. Principalmente, pois costumam ser uma surpresa para iniciantes em C++, tendo em vista que não existe uma resposta bem definida para nenhuma delas.

O tamanho de um int não é padronizado em C++. O termo definido pela implementação (implementation-defined) é utilizado pelo padrão para abordar situações como essa. Portanto, o resultado de sizeof(int) pode não ser o mesmo em plataformas/ambientes diferentes. (No caso de int o tamanho sugerido é aquele natural da arquitetura.) Em um hardware embarcado, por exemplo, pode ser 2 bytes. Já em um computador de grande porte pode ser 8 bytes.

Além disso, também não é correto assumir que sizeof(long) é maior do que sizeof(int). Novamente, o padrão C++ não faz exigências quanto a isso. Basicamente, a única restrição imposta aos tipos fundamentais é de que sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long). Replico abaixo o texto oficial do ISO/IEC 14882 (2003).

        ”There are four signed integer types: “signed char”, “short int”, “int”, and “long int.” In this list, each type provides at least as much storage as those preceding it in the list.

A última pergunta, que também é o título do post, tem resposta positiva. Ou seja, sizeof(char) é sempre igual a 1 byte. No entanto, não se empolgue muito em criar códigos mirabolantes através de manipulações de bits baseados no tamanho do char. Apesar da garantia acima existir, o padrão C++ não define qual o número de bits que um byte possui. Novamente, o termo implementation-defined se aplica:

        ”A byte is at least large enough to contain any member of the basic execution character set and is composed of a contiguous sequence of bits, the number of which is implementation-defined.

É claro que na maioria dos casos um byte consiste de 8 bits. (Por coincidência, me lembro de ter lido recentemente algo sobre uma plataforma na qual o byte era composto de 7 bits. Não lembro onde nem os detalhes…) Logo, talvez essa “preocupação” nao tenha tanta justificativa do ponto de vista prático. Mas de qualquer forma, o conhecimento é sempre útil!

Continuando a série de posts sobre conceitos (concepts) no C++0x, agora é hora de apresentar outro elemento importante. No último post, expliquei como especificar um contexto restrito no qual apenas tipos LessThanComparable (comparáveis por menor) são permitidos. Tais tipos devem suportar o operator< que recebe dois parâmetros e retorna um bool. Porém, nem sempre trabalhamos com funções associadas que operam somente sobre tipos primitivos. Inclusive, em muitos casos nem sequer sabemos exatamente quais os tipos dos dados em questão. Para resolver esses e outros problemas existem os tipos associados.

O objetivo dos tipos associados é justamente introduzir abstrações pelas quais podemos suportar funções associadas de maneira natural. Para ilustrar, considere o exemplo do post anterior no qual criei uma classe chamada book. Suponha que essa classe faça parte de uma aplicação que modela o sistema de uma livraria. Nela, todos os itens como livros, revistas, jornais ou similares são caracterizados como sendo materias de leitura.

A aplicação da livraria, assim como qualquer outra, possui regras de negócio que devem ser aplicadas a todos os materiais de leitura. Mas como estamos no mundo da programação genérica, ao invés de fazer com que todos eles herdem de uma classe base comum farei com que todos modelem um conceito comum, chamado ReadableMaterial.

Para que um item seja considerado um ReadableMaterial ele precisa satisfazer alguns requisitos como, por exemplo, saber o seu número de páginas. No caso da classe book do post anterior assumi que essa informação era armazenada em um membro do tipo int. No entanto, para promover um tratamento uniforme entre os variados materiais de leitura é necessário adotar uma abstração genérica que represente o tipo de dados que cada um deles utiliza para armazenar seu número de páginas. Afinal de contas, nessa aplicação hipotética, enquanto os livros utilizam um int os jornais e revistas utilizam um short. Já as enciclopédias utilizam um std::size_t. Além disso, pode aparecer um novo material de leitura que utilize um tipo de dados personalizado, definido pelo próprio usuário. (Lembre que isso é apenas um exemplo de caráter didático.) Portanto, a solução é especificar, além da função associada, um tipo associado ao conceito ReadableMaterial.

Uma outra informação que gostaria de ter sobre os materiais de leitura é se determinada página contém alguma ilustração. Assim como o número de páginas, esse requisito pode ser introduzido no conceito através de uma função associada. Com isso, a definição de ReadableMaterial fica conforme o código abaixo.

concept ReadableMaterial <typename T>
{
  typename page_size_type;

  page_size_type num_pages(T const&);
  bool is_illustrated_page(T const&, page_size_type);
}

Agora, a próxima etapa é justamente criar mapas de conceito entre ReadableMaterial e as classes que o modelam, como book, magazine, newspaper, entre outras. Abaixo, mostro apenas para book, mas os outros seriam similares. Note que o conceito não requer as funções associadas num_pages e is_illustrated_page como membros do tipo modelo (que, de fato, pode ser feito). Logo, preciso realizar o mapeamento explícito dessas funções (no caso, utilizando as próprias funções membro de book).

class book
{
public:
  int num_pages() const { return num_pages_; }
  bool is_illustrated_page(int page_num) const { /*...*/ }

private:
  int num_pages_;
};

concept_map ReadableMaterial<book>
{
  typedef int page_size_type;

  int num_pages(book const& b)
  { return b.num_pages(); }

  bool is_illustrated_page(book const& b, int page_num)
  { return b.is_illustrated_page(page_num); }
}

Tendo o conceito e seus respectivos modelos definidos, posso implementar uma função genérica simples que conta o número de páginas com ilustrações de um ReadableMaterial. Preste atenção no código e tente identificar se ainda está faltando algo nesta abordagem.

template <typename T>
  requires ReadableMaterial<T>
ReadableMaterial<T>::page_size_type count_illustrated_pages(T const& t)
{
  ReadableMaterial<T>::page_size_type count = 0;
  for (ReadableMaterial<T>::page_size_type i = 0; i < num_pages(t); ++i)
    if (is_illustrated_page(t, i))
      ++count;
  return count;
}

Provavelmente, os leitores que já captaram a essência da programação baseada em conceitos detectaram pequenos problemas. No primeiro post da série, tentei deixar claro que uma grande vantagem desse paradigma é exatamente a imposição contratual de certas regras, tanto para o autor quanto para o usuário de templates. Eu, como autor de count_illustrated_pages, violei parte desse contrato. O motivo é o seguinte: Informei aos usuários que tipos que modelam ReadableMaterial poderiam ser utilizados na função acima. Porém, em sua implementação utilizei construções sintáticas que não são requisitos desse conceito. Ou seja, count_illustrated_pages depende que page_size_type satisfaça requisitos que não são exigidos em ReadableMaterial. São eles:

- Construção a partir de um inteiro;
- Comparação por menor;
- Operador de pré-incremento;
- Construtor de cópia;
- Destrutor.

Esses pequenos itens não são motivo de dores de cabeça. Há uma forma simples de fazer o exemplo funcionar. Basta adicionar os requisitos acima ao tipo associado page_size_type de ReadableMaterial. Felizmente, há um conceito chamado ArithmeticLike no novo padrão C++ que já os encapsula. Portanto, há basicamente duas formas de contornar a situação. A primeira delas consiste de adicionar a restrição de ArithmeticLike sobre page_size_type na função count_illustrated_pages, conforme mostrado abaixo. (Note a notação simplificada na qual especifico T como sendo um ReadableMaterial ao invés de um typename convencional.)

template <ReadableMaterial T>
  requires ArithmeticLike<T::page_size_type>
T::page_size_type count_illustrated_pages(T const& t)
{
  T::page_size_type count = 0;
  for (T::page_size_type i = 0; i < num_pages(t); ++i)
    if (is_illustrated_page(t, i))
      ++count;
  return count;
}

A segunda delas, que considero a melhor opção, é adicionar a restrição de ArithmeticLike sobre page_size_type diretamente em ReadableMaterial. Nesse caso, não é necessário colocar a restrição de ArithmeticLike sobre a função, já que agora ela é parte integral do próprio conceito.

concept ReadableMaterial <typename T>
{
  typename page_size_type;

  page_size_type num_pages(T const&);
  bool is_illustrated_page(T const&, page_size_type);

  requires ArithmeticLike<page_size_type>; //Novo requisito!
}

Até a próxima!

Leandro T. C. Melo

Começarei este post com trechos de código e, em seguida, farei uma pergunta. Considere o struct information abaixo.

struct information
{
  typedef int value_type;
  value_type get_info() const { /* ... */ }
};

Notem que ele simplesmente define um tipo e uma função membro não-virtual. Assim como membros estáticos, definições de tipo e funções não-virtuais não ocupam espaço em uma classe. Porém, ao declarar um membro do tipo information no template de classe simple, abaixo, observamos que 4 bytes adicionais são alocados. (Utilize sizeof para testar.)

template <class information_t>
class simple
{
public:
  simple(information_t const& info, int data) :
    info_(info), data_(data)
  {}

private:
  information_t info_; //Gera 4 bytes extras em simple.
  int data_;
};

//...

simple<information> s(information(), 10);

Se simple tivesse apenas o inteiro data_ seu tamanho seria de 4 bytes (naturalmente, considerando o ambiente que estou compilando), mas com o membro info_ seu tamanho vai para 8 bytes. Qual a razão desse comportamento?

A resposta é a seguinte: O padrão C++ não permite a existência de tipos com tamanho zero, mesmo sendo classes vazias como o struct information. Se não acreditar, experimente criar uma classe sem absolutamente nenhum membro e verifique seu sizeof. Pode parecer contraditório, mas existem motivos importantes para essa restrição. Como seria, por exemplo, um array de tipos de tamanho zero? Imaginem as operações aritméticas de ponteiros sobre ele? Confuso, não?

Acontece que espaço desnecessário em memória é caro em determinadas aplicações. Neste mesmo blog, em um post anterior, mencionei como estratégias bem conhecidas de alinhamento e preenchimento podem resultar em uso mais eficiente de espaço. Portanto, o ideal é encontrar alguma forma de contornar essa situação. No paradigma de programação genérica, por exemplo, é bastante comum classes com caraterísticas bem similares as do struct information.

Pensando nisso, os envolvidos na padronização do C++ deixaram uma “brecha” interessante. Apesar de todos os tipos serem obrigados a terem tamanho maior que zero, quando classes vazias são utilizadas como classes bases não é necessário que nenhum espaço seja alocado para elas. Obviamente, a condição vale desde que haja a garantia que a classe base vazia não seja alocada em um mesmo endereço de outros objetos, inclusive derivados da própria hierarquia. Essa estratégia é conhecida como otimização de classe base vazia, do inglês Empty Base Class Optimization (EBCO).

Creio que a maioria dos compiladores profissionais implementam essa otimização. Se não me engano, tanto o Microsoft Visual C++ quanto o GCC já a fazem há bastante tempo. Logo, desenvolvedores de bibliotecas C++ ricas em templates (o que não é nada raro atualmente) devem ficar atentos para as oportunidades de otimização. No caso do exemplo deste post, como poderíamos usufruir desse recurso?

O primeiro passo é garantir que o struct information seja uma classe base, eliminando, assim, o espaço desnecessário em memória. Uma alternativa é fornecer um template de classe que agregue tanto a classe vazia quanto um dado qualquer. Tal template deve herdar da classe vazia conforme o código abaixo.

template <class base_t, class data_t>
struct ebco : base_t
{
  data_t data_;
  ebco(base_t const& base, data_t const& data) :
    base_t(base), data_(data)
  {}
};

Agora, ao invés de declarar o membro info_ e o membro data_, declararamos apenas o template ebco parametrizado com information e o inteiro que representa o dado. O template de classe optimized_simple faz exatamente isso.

template <class information_t>
class optimized_simple
{
public:
  optimized_simple(information_t const& info, int data) :
    optimization_(info, data)
  {}

private:
  ebco<information_t, int> optimization_ ;
};

Pronto! Comparando os retornos de sizeof(simple) e sizeof(optimized_simple) obtemos 8 bytes para o primeiro e 4 bytes para o segundo. Ou seja, para esse caso específico reduzimos o tamanho de um tipo pela metade.

Creio que a maioria dos programadores C++ saiba que o std::cout é um objeto do tipo std::ostream. Sendo que std::ostream é nada mais do que uma definição do template de classe std::basic_ostream com o tipo char. Ou seja:

typedef basic_ostream<char> ostream;

Por outro lado, já não é qualquer programador que sabe o que é o std::endl que comumente acompanha um std::cout. Um objeto? Uma função? Afinal, o que acontece exatamente quando o compilador encontra um std::endl?

std::cout << "Invocando um manipulador..." << std::endl;

Tecnicamente, o std::endl é um manipulador (manipulator). Manipuladores são “componentes” que podem ser utilizados em conjunto com os operadores de entrada e saída de stream afim de… ora, manipular o stream. Provavelmente, você conhece outros manipuladores como, por exemplo, o std::boolalpha ou o std::flush.

Como é implementado um manipulador? Bom, isso depende. Manipuladores que não possuem parâmetros, como os descritos no parágrafo acima, são ponteiros de funções (function pointers). Já os manipuladores que possuem parâmetros, como std::setw() e std::setfill(), entre outros, são simplesmente objetos de determinada classe. Nesse post, mostrarei a implementação de um manipulador simples e sem parâmetros.

O padrão C++ disponibiliza quatro tipos de ponteiros de funções a serem utilizados pelos manipuladores de streams. Eles são membros dos templates basic_istream e basic_ostream e são invocados por uma sobrecarga convencional dos operadores de deslocamento (shift): operator<< e operator>>. Lembre que a hierarquia de classes da biblioteca C++ de IOStreams começa com a classe ios_base. Em seguida, aparece a classe basic_ios, que na verdade é um template de classe parametrizado pelo tipo do caractere e traits de caracteres. Herdando diretamente de basic_ios e igualmente parametrizadas, estão os templates de classe basic_istream e basic_ostream. Abaixo, na definição parcial de basic_istream, charT e traits são, respectivamente, os parâmetros correspondentes ao tipo de caractere e ao traits.

//Namespace std naturalmente.
template <class charT, class traits = char_traits<charT> >
class basic_istream : virtual public basic_ios<charT, traits>
{
public:
  //...

  //Para manipuladores que acessam apenas ios_base.
  basic_istream<charT,traits>&
  operator>>
  (ios_base& (*pf)(ios_base&));

  //Para manipuladores que acessam basic_ios.
  basic_istream<charT,traits>&
  operator>>
  (basic_ios<charT,traits>& (*pf)(basic_ios<charT,traits>&));

  //Para manipuladores que dependem de istream.
  basic_istream<charT,traits>&
  operator>>
  (basic_istream<charT,traits>& (*pf)(basic_istream<charT,traits>&));
};

No parágrafo anterior eu disse que são disponibilizadas sobrecargas para quatros tipos de ponteiros de função. A quarta sobrecarca é equivalente à terceira de basic_istream mostrada acima. Porém, ela está diponível em basic_ostream e é voltada para streams de saída. O importante de ser observado é que todos os tipos de ponteiros de funções recebem e retornam um stream. Portanto, qualquer função que tenha uma assinatura consistente com uma das definições pode ser usada como um manipulador. Simples, não?

Aqui vai, então, a implementação e utilização do meu super manipulator. Ele não faz grandes coisas. Basicamente, é um std::endl incrementado que, além do caractere de fim de linha, também adiciona um caractere de tab para a próxima linha. Seu nome é endlplacet (end line and place tab). Ainda não consegui nenhuma utilidade prática para ele…

#include <iostream>

template <class charT, class traits>
inline std::basic_ostream<charT, traits>&
endlplacet(std::basic_ostream<charT, traits>& os)
{
    os.put(os.widen('\n'));
    os.put(os.widen('\t'));
    os.flush();

    return os;
}

int main()
{
    std::cout << "Feliz 2009," << endlplacet << "Leandro Melo.";

    return 0;
}

Leandro T. C. Melo

Postagens Antigas »