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.