Quão útil é o dimensionamento "verdadeiro" de variáveis ​​de C?

9

Uma coisa que sempre me pareceu intuitivamente um recurso positivo do C (bem, na verdade, de suas implementações como gcc, clang, ...) é o fato de ele não armazenar nenhuma informação oculta ao lado de suas próprias variáveis ​​em tempo de execução. Com isso, quero dizer que, por exemplo, se você quiser uma variável "x" do tipo "uint16_t", pode ter certeza de que "x" ocupará apenas 2 bytes de espaço (e não carregará nenhuma informação oculta como seu tipo, etc. .). Da mesma forma, se você deseja uma matriz de 100 números inteiros, pode ter certeza de que é tão grande quanto 100 números inteiros.

No entanto, quanto mais eu estou tentando criar casos de uso concretos para esse recurso, mais me pergunto se ele realmente tem alguma vantagem prática. A única coisa que eu consegui chegar até agora é que ele obviamente precisa de menos RAM. Para ambientes limitados, como chips AVR etc., essa é definitivamente uma grande vantagem, mas para os casos de uso de desktop / servidor todos os dias, isso parece ser irrelevante. Outra possibilidade em que estou pensando é que ela pode ser útil / crucial para acessar o hardware ou talvez mapear regiões de memória (por exemplo, para saída VGA e similares) ...?

Minha pergunta: existem domínios concretos que não podem ou podem ser implementados com muita dificuldade sem esse recurso?

PS Por favor, diga-me se você tem um nome melhor para ele! ;)

Thomas Oltmann
fonte
@gnat Acho que entendo qual é o seu problema. É porque pode haver várias respostas, certo? Bem, eu entendo que esta questão pode não se adequar as obras Stackexchange maneira, mas eu honestamente não sei onde perguntar elsewise ...
Thomas Oltmann
11
@lxrec RTTI é armazenado na vtable e os objetos armazenam apenas um ponteiro na vtable. Além disso, os tipos só têm RTTI se já tiverem uma tabela v, porque eles têm uma virtualfunção de membro. Portanto, o RTTI nunca aumenta o tamanho de nenhum objeto, apenas aumenta o binário por uma constante.
3
@ThomasOltmann Todo objeto que possui métodos virtuais precisa de um ponteiro vtable. Você não pode ter os métodos virtuais de funcionalidade sem isso. Além disso, você opta explicitamente por ter métodos virtuais (e, portanto, uma vtable).
11
@ThomasOltmann Você parece muito confuso. Não é um ponteiro para um objeto que carrega um ponteiro de tabela, é o próprio objeto. Ou seja, T *sempre tem o mesmo tamanho e Tpode conter um campo oculto que aponta para a tabela. E nenhum compilador C ++ inseriu vtables em objetos que não precisam deles.

Respostas:

5

Existem vários benefícios, o óbvio é o tempo de compilação para garantir que itens como parâmetros de função correspondam aos valores passados.

Mas acho que você está perguntando sobre o que está acontecendo em tempo de execução.

Lembre-se de que o compilador criará um tempo de execução que incorpora o conhecimento dos tipos de dados nas operações que ele executa. Cada parte dos dados na memória pode não ser autoexplicativa, mas o código sabe inerentemente o que são esses dados (se você fez seu trabalho corretamente).

Em tempo de execução, as coisas são um pouco diferentes do que você pensa.

Por exemplo, não assuma que apenas dois bytes são usados ​​quando você declara uint16_t. Dependendo do processador e do alinhamento de palavras, ele pode ocupar 16, 32 ou 64 bits na pilha. Você pode achar que sua variedade de shorts consome muito mais memória do que o esperado.

Isso pode ser problemático em determinadas situações em que você precisa fazer referência a dados em compensações específicas. Isso acontece ao se comunicar entre dois sistemas que possuem arquiteturas de processador diferentes, por meio de um link sem fio ou por arquivos.

C permite especificar estruturas com granularidade no nível de bit:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

Essa estrutura tem três bytes de comprimento, com um short definido para iniciar em um deslocamento ímpar. Ele também precisará ser compactado para ser exatamente como você o definiu. Caso contrário, o compilador alinhará os membros por palavra.

O compilador irá gerar código nos bastidores para extrair esses dados e copiá-los em um registro para que você possa fazer coisas úteis com ele.

Agora você pode ver que toda vez que meu programa acessar um membro da estrutura myMessage, ele saberá exatamente como extraí-lo e operá-lo.

Isso pode se tornar problemático e difícil de gerenciar ao se comunicar entre diferentes sistemas com diferentes versões de software. Você deve projetar cuidadosamente o sistema e o código para garantir que ambos os lados tenham exatamente a mesma definição dos tipos de dados. Isso pode ser bastante desafiador em alguns ambientes. É aqui que você precisa de um protocolo melhor que contenha dados autoexplicativos, como os Buffers de Protocolo do Google .

Por fim, você faz questão de perguntar o quanto isso é importante no ambiente de desktop / servidor. Realmente depende da quantidade de memória que você planeja usar. Se você estiver executando algo como processamento de imagem, poderá acabar usando uma grande quantidade de memória que pode afetar o desempenho do seu aplicativo. Definitivamente, isso sempre é uma preocupação no ambiente incorporado, onde a memória é restrita e não há memória virtual.

Tereus Scott
fonte
2
"Você pode achar que sua variedade de shorts consome muito mais memória do que o esperado". Isso está errado em C: é garantido que as matrizes contêm seus elementos de maneira livre de falhas. Sim, a matriz precisa ser alinhada corretamente, assim como uma única short. Mas este é um requisito único para o início da matriz, o restante é automaticamente alinhado corretamente por ser consecutivo.
cmaster - restabelecer monica
Além disso, a sintaxe do preenchimento está incorreta uint8_t padding: 6;, assim como os dois primeiros bits. Ou, mais claramente, apenas o comentário //6 bits of padding inserted by the compiler. A estrutura, como você a escreveu, tem um tamanho de pelo menos nove bytes, não três.
cmaster - restabelece monica
9

Você encontrou um dos únicos motivos pelos quais isso é útil: mapear estruturas de dados externas. Isso inclui buffers de vídeo mapeados na memória, registros de hardware, etc. Eles também incluem dados transmitidos intactos fora do programa, como certificados SSL, pacotes IP, imagens JPEG e praticamente qualquer outra estrutura de dados que tenha uma vida persistente fora do programa.

Ross Patterson
fonte
5

C é uma linguagem de baixo nível, quase um montador portátil, portanto suas estruturas de dados e construções de linguagem estão próximas do metal (as estruturas de dados não têm custos ocultos - exceto restrições de preenchimento, alinhamento e tamanho impostas pelo hardware e pela ABI ). Portanto, C realmente não possui digitação dinâmica nativamente. Mas se você precisar, você pode adotar uma convenção de que todos os seus valores são agregados, começando com algumas informações de tipo (por exemplo, algumas enum...); utilização union-s e (para a matriz como as coisas) membro da matriz flexível em structcontendo também o tamanho da matriz.

(ao programar em C, é de sua responsabilidade definir, documentar e seguir convenções úteis - principalmente pré e pós-condições e invariantes; também a alocação dinâmica de memória C requer convenções freeexplicativas sobre quem deve alguma malloczona de memória acumulada)

Portanto, para representar valores que são inteiros ou cadeias de caracteres em caixa, ou algum tipo de símbolo semelhante ao esquema ou vetores de valores, você usará conceitualmente uma união marcada (implementada como uma união de ponteiros) sempre começando pelo tipo de tipo -, por exemplo:

enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
   const void* vptr; // generic pointer, e.g. to free it
   enum value_kind_en* vkind; // the value of *vkind decides which member to use
   struct intvalue_st* vint;
   struct strvalue_st* vstr;
   struct symbvalue_st* vsymb;
   struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE  ((value_t){NULL})
struct intvalue_st {
  enum value_kind_en kind; // always V_INT for intvalue_st
  int num;
};
struct strvalue_st {
  enum value_kind_en kind; // always V_STRING for strvalue_st
  const char*str;
};
struct symbvalue_st {
  enum value_kind_en kind; // V_SYMBOL
  struct strvalue_st* symbname;
  value_t symbvalue;
};
struct vectvalue_st {
  enum value_kind_en kind; // V_VECTOR;
  unsigned veclength;
  value_t veccomp[]; // flexible array of veclength components.
};

Para obter o tipo dinâmico de algum valor

enum value_kind_en value_type(value_t v) {
  if (v.vptr != NULL) return *(v.vkind);
  else return V_NONE;
}

Aqui está um "elenco dinâmico" para vetores:

struct vectvalue_st* dyncast_vector (value_t v) {
   if (value_type(v) == V_VECTOR) return v->vvect;
   else return NULL;
}

e um "acessador seguro" dentro de vetores:

value_t vector_nth(value_t v, unsigned rk) {
   struct vectvalue_st* vecp = dyncast_vector(v);
   if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
   else return NULL_VALUE;
}

Você geralmente define a maioria das funções curtas acima, como static inlineem algum arquivo de cabeçalho.

BTW, se você pode usar o coletor de lixo da Boehm, poderá codificar com bastante facilidade em um estilo de nível superior (mas não seguro), e vários intérpretes do Scheme são feitos dessa maneira. Um construtor de vetor variável pode ser

value_t make_vector(unsigned size, ... /*value_t arguments*/) {
   struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
   vec->kind = V_VECTOR;
   va_args args;
   va_start (args, size);
   for (unsigned ix=0; ix<size; ix++) 
     vec->veccomp[ix] = va_arg(args,value_t);
   va_end (args);
   return (value_t){vec};
}

e se você tiver três variáveis

value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;

você pode construir um vetor deles usando make_vector(3,v1,v2,v3)

Se você não quiser usar o coletor de lixo de Boehm (ou criar o seu próprio), tenha muito cuidado ao definir destruidores e documentar quem, como e quando a memória deve ser free-d; veja este exemplo. Então você pode usar malloc(mas testar contra a falha) em vez de GC_MALLOCacima, mas precisa definir e usar com cuidado alguma função destruidoravoid destroy_value(value_t)

A força de C é ter um nível baixo o suficiente para tornar possível o código acima e definir suas próprias convenções (específicas ao seu software).

Basile Starynkevitch
fonte
Eu acho que você não entendeu minha pergunta. Não quero digitação dinâmica em C. Fiquei curioso para saber se essa propriedade específica de C é útil.
Thomas Oltmann
Mas a qual propriedade exata de C você está se referindo? Estruturas de dados encontram-se perto de C para o metal, de modo que não têm custos ocultos (com excepção de alinhamento e tamanho constrangimentos)
Basile Starynkevitch
Exatamente isso: /
Thomas Oltmann 17/01
C foi inventado como uma linguagem de baixo nível, mas quando as otimizações são ativadas nos compiladores, como o gcc processa uma linguagem que usa a sintaxe de baixo nível, mas não fornece de maneira confiável o acesso de baixo nível às garantias comportamentais fornecidas pela plataforma. Um precisa sizeof para usar malloc e memcpy, mas o uso de cálculos de endereços mais sofisticados podem não ser suportados em "moderno" C.
supercat