Post

Infdev #02 - Como as versões de C/C++ funcionam.

Infdev #02 - Como as versões de C/C++ funcionam.

Alguns dos fatores que podem confundir um novato no ecossistema de C/C++ estão relacionados às versões. Muitos estão acostumados a uma linguagem ser associada a uma empresa e ter apenas um compilador ou interpretador principal: Python tem a Python Foundation com CPython, Java tem a Oracle com a JVM, Rust tem a Rust Foundation com o RustC, e que se uma nova versão é lançada, é esperado que mudem para a nova versão. No entanto, pouco se fala sobre como uma linguagem não está presa a um compilador/interpretador único. Juridicamente falando, uma empresa não pode ter a exclusividade para criar um programa para ler o código fonte na linguagem que ela criou. A maioria das linguagens possui alternativas: Python tem o PyPy, Java tem a IBM J9 JVM, e inúmeras linguagens compiladas como Rust, Go, Ada… podem ser compiladas pelo GCC. A especificação de uma linguagem é definida pela empresa ou grupo que a criou, porém a implementação é livre para qualquer um que tenha acesso a essa especificação. Às vezes, a empresa que cria a especificação tem seu próprio compilador ou interpretador, e às vezes não, e C/C++ se enquadra no segundo caso.

The C/C++ Foundation?

O “dono” de C/C++ é a ISO (de forma mais específica, a parte da ISO responsável por C/C++ é a ISO/IEC JTC1/SC22/WG14). A ISO (Organização Internacional de Normalização) é uma organização responsável pela padronização de uma infinidade de coisas. Eles são responsáveis por criar as especificações de C/C++ e o que deve existir nas bibliotecas padrão, e apenas isso. São eles que definem como um for loop deve funcionar, como uma função pode ser declarada, como cada tipo funciona, quais bibliotecas devem vir por padrão e quais funções devem existir em cada biblioteca. Tudo isso é ditado por eles, porém eles não implementam nada, apenas criam o documento que define todos os aspectos da linguagem. Quem fica responsável pela implementação são os desenvolvedores dos compiladores: a GNU com o GCC, Apple com o CLang, Microsoft com o MSVC… Porém, algumas partes das especificações podem ser ignoradas. Por exemplo, no padrão C++11, existia na a implementação de um Garbage Collector, o que removeria o gerenciamento manual de memória do C++. Porém, nenhum compilador implementou tal garbage collector, pois, como eu disse no Infdev #1, o gerenciamento de memória manual é algo intrínseco a C/C++. E, no padrão C++23, a especificação do Garbage Collector foi removida.

Para resumir, quem é responsável por dizer “isso é tudo que você pode fazer no C/C++ por padrão” é o comitê da ISO, e quem implementa são os mantenedores dos compiladores, mesmo que, às vezes, algumas coisas possam ser diferentes do que a ISO deseja. Todos os principais compiladores permitem que você defina qual versão quer utilizar com a flag -std={version}, então, se você não gosta de alguma versão, não precisa utilizar ela.

Deixando claro: sempre que eu falar neste post sobre “nessa nova versão foi adicionado/mudado/removido…”, estou me referindo ao documento de padronização disponibilizado pela ISO, e não à implementação dos compiladores.

Versões de C

C ao longo do tempo teve 5 grandes verções:

K&R C (1978)

Se você já ouviu falar de algum livro sobre C, a chance de ter sido o The C Programming Language. Esse foi o primeiro livro sobre C, criado por Brian Kernighan e Dennis Ritchie (daí o K&R), e se tornou o primeiro padrão para “o que é C”. Esse padrão possui algumas coisas específicas que mudaram logo em seguida nas próximas versões, sejam questões de sintaxe ou de padrão na escrita do código.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int power(int m, int n);

main()
{
    int i;
    for (i = 0; i < 10; ++i)
    printf("%d %d %d\n", i, power(2,i), power(-3,i));
    return 0;
}

int power(int base, int n)
{
    int i, p;
    p = 1;
    
    for (i = 1; i <= n; ++i)
        p = p * base;
    return p;
}
  1. Todas as variáveis precisam ser declaradas antes de serem usadas, comumente no começo da função.
  2. Funções que retornam int podem ter o tipo omitido.
  3. Declarar funções no começo do código e só depois implementá-las.
  4. E, para mim, o pior: pular linha quando for abrir parênteses – e isso é algo que até hoje é costume em alguns devs de C/C++ e até C#.

C90

C90(também chamada de C89(também conhecida como ANSI C(também conhecida com ISO C) é até hoje uma das versões mais utilizadas de C. Os quatro nomes citados anteriormente são tecnicamente diferentes, porém são basicamente a mesma coisa com nenhuma mudança prática, apenas algumas revisões de texto. Algumas das mudanças do K&R para o C90 são:

  1. Inclusão de void pointers (terei em breve um post sobre ponteiros)
  2. Novas funcionalidades para criação de macros
  3. Não é mais necessário definir as variáveis antes de utilizá-las
  4. Criação de novas funções nas bibliotecas padrão (basicamente todas as versões adicionam funções ou novas bibliotecas às bibliotecas padrão – vou ignorar isso nas próximas)

É difícil encontrar material preciso sobre as mudanças feitas aqui, tanto pela idade quanto pela quantidade de diferentes versões de C e da biblioteca padrão que foram implementadas nesse período de 1980 a 1990, mas, no geral, C90 é a versão que define C.

Assim fica o código anterior na nova versão:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int power(int base, int n) {
    int p = 1;
        
    for (int i = 1; i <= n; ++i)
        p = p * base;

    return p;
}

int main(){
    for (int i = 0; i < 10; ++i)
        printf("%d %d %d\n", i, power(2, i), power(-3, i));

    return 0;
}

C99

C99 é o grande padrão de C até hoje, superando até o C90. C99 teve, dentre suas mudanças:

  1. Adição de tipos novos como booleanos e números complexos
  2. Adição de listas com tamanho que pode ser determinado em tempo de execução
  3. Suporte ao padrão IEEE 754-1985 para números de ponto flutuante

Não tenho como confirmar, mas acredito que C99 foi a maior atualização para a linguagem. Não à toa, provavelmente é até hoje a versão com maior uso entre os padrões. C99 tem tudo que é preciso para construir com qualidade qualquer software que você queira, e isso é parte dos motivos pelos quais muitos desenvolvedores se fixam no C99 – as coisas que foram adicionadas daqui para frente acabam sendo consideradas desnecessárias ou bloatware por muitos.

C11

C11 é uma versão um tanto controversa, mas antes vamos falar sobre algumas coisas que ela adicionou:

  1. Suporte nativo a threads
  2. Funções para tratar alinhamento de memória
  3. Adição de _Generic para sistemas de macros
  4. Funcionalidades para verificação de limites

Agora sobre os problemas: tudo que foi adicionado parece ótimo, porém as threads demoraram muito para ser implementadas de forma correta pela maioria dos compiladores – em parte pela já existência de bibliotecas superiores para lidar com threads, em parte pelo trabalho que seria criar uma biblioteca nativa para lidar com threads de forma a manter o cross-platform. O sistema de generics foi muito visto como algo desnecessário, por mais que seja útil, e a verificação de limites foi, de longe, o maior problema na especificação, com quase nenhum compilador moderno implementando de forma completa ou mesmo tentando. Os principais motivos são retrocompatibilidade e, ironicamente, segurança – a grande mudança para aumentar a segurança de C veio com mais problemas de segurança do que resolvia.

Basicamente, todo mundo que usava C já tinha suas próprias formas de garantir a verificação de limites, até porque não é algo difícil de se implementar sozinho, e a especificação possuía um sistema que seria extremamente problemático e difícil de utilizar em bases de código já existentes. Esse problema de retrocompatibilidade era sabido, e por isso a funcionalidade foi posta como opcional, o que faria com que a maioria dos devs simplesmente ignorasse a funcionalidade para garantir que não haveria problemas com alguma dependência, tornando-a inútil.

C11 foi uma versão que adicionou poucas coisas realmente úteis, como melhor suporte a Unicode e a capacidade de criar estruturas com unions anônimas:

1
2
3
4
5
6
7
struct MyStruct {
    int tag; 
    union { 
        float x;
        int n;
    } un;
};

Porém, os problemas da especificação acabam sendo maiores do que as suas vantagens aos olhos de alguns desenvolvedores, ou simplesmente são irrelevantes. Por sorte, a próxima versão resolveu alguns problemas de C11.

C23

C23 é a atual versão de C, provavelmente a segunda maior versão depois de C99, adicionando muitas coisas e removendo algumas outras. Começando por algumas coisas que ela adicionou:

  1. Criação de booleanos como tipos primitivos
  2. Melhoria na criação de macros
  3. Atualização em várias bibliotecas como math, string e stdlib, reescrevendo funções antigas e criando novas

E agora para as coisas que ela removeu, basicamente tudo que foi adicionado no C11:

  1. As funções de alinhamento foram removidas e/ou reimplementadas
  2. Muitas das implementações de threads
  3. Algumas das funcionalidades de macros de C11

C23 foi um grande reflexo sobre o C11, com poucas mudanças drásticas na base da linguagem, porém muitas mudanças nas implementações das bibliotecas padrão e remoção de muitas funcionalidades não adotadas pela comunidade do C11. C23 ainda é um padrão em aberto, e pode e provavelmente sofrerá mudanças ao passar do tempo.

Resumo

Se você for mexer em algum projeto em C e ninguém lhe disser qual é o padrão sem você precisar perguntar, assuma que estão utilizando C99. Porém, caso você for começar um projeto do zero e não queira utilizar C99 ou versões anteriores, pode pular direto para o C23.

Ainda existem outras versões de C, como, por exemplo, as versões GNU, que são as versões normais da ISO, porém implementadas pela equipe da GNU com as bibliotecas padrão deles. O Linux, por exemplo, é feito com as versões GNU. Outra versão mais específica é o Embedded C, para lidar com sistemas embarcados, disponibilizando funcionalidades para trabalhar com bancos de memória, endereços absolutos de memória e outras coisas. E sistemas como videogames e computadores antigos terão suas próprias versões de C, com suas próprias bibliotecas.

No final das contas, a versão que você escolher não vai influenciar muito no resultado final, pois a grande maioria do código que você for escrever usará funcionalidades que já estão presentes na linguagem desde o começo. As maiores mudanças entre as versões acabam sendo novidades nas bibliotecas padrão, e poucas são as mudanças na linguagem em si. Então, seu código acaba sendo 80% habilidade com o básico de C e 20% do seu conhecimento sobre a versão que você está utilizando.

Sobre C++

Eu gostaria de falar sobre os padrões de C++, porém, diferente de C, que possui poucas versões e não atualiza com tanta frequência ao longo do tempo, C++ adota uma forma muito mais tradicional, lançando novas versões a cada 3 anos. Portanto, falar sobre cada versão de forma individual seria um exercício de futilidade. C++ é conhecido por ser uma linguagem com uma quantidade absurda de coisas por padrão, e muitas delas são mal definidas ou simplesmente mal implementadas. Diferente de C, novas versões de C++ não são tão fixadas em manter 100% da retrocompatibilidade com versões antigas.

Muitos desgostam de C++ e utilizam apenas C justamente por verem a grande parte das novas funcionalidades de C++ em relação a C como algo desnecessário, e as coisas adicionadas ao longo das novas versões como mais entulho em um guarda-roupa já sem espaço. Eu me encontro nesse grupo, porém sei que penso assim porque prefiro linguagens mais simples a linguagens complexas. C++ é um padrão no mercado até maior do que C e possui grande uso de suas novidades e várias vantagens em relação a C, apenas as filosofias de cada linguagem que são diferentes.

This post is licensed under CC BY 4.0 by the author.

Trending Tags