Funções que retornam strings, bom estilo?

Em meus programas em C, geralmente preciso encontrar uma maneira de fazer uma representação de string de meus ADTs. Mesmo que eu não precise imprimir a string para a triagem de nenhuma forma, é ótimo ter esse método de depuração. Portanto, esse tipo de função costuma aparecer.

char * mytype_to_string( const mytype_t *t ); 

Na verdade, percebo que tenho (pelo menos) três opções aqui para lidar com a memória para que a string retorne.

Alternativa 1: Armazenar a string de retorno em um array de char estático na função . Não preciso pensar muito, exceto que a string é substituída a cada chamada. O que pode ser um problema em algumas ocasiões.

Alternativa 2: alocar a string no heap com malloc dentro da função. Muito bom, já que não precisarei pensar no tamanho de um buffer ou na substituição. No entanto, tenho que me lembrar de liberar () a string quando terminar e, em seguida, também preciso atribuir a uma variável temporária de modo que Eu posso liberar. E então a alocação de heap é realmente muito mais lenta do que a alocação de pilha, portanto, será um gargalo se isso for repetido em um loop.

Alternativa 3: Passe o ponteiro para um buffer e deixe o chamador alocar esse buffer. Como:

char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen ); 

Isso traz mais esforço para o chamador. Também noto que essa alternativa me dá uma outra opção na ordem dos argumentos. Qual argumento devo ter primeiro e último? (Na verdade, seis possibilidades)

Então, qual devo preferir? Qualquer por quê? Existe algum tipo de padrão não escrito entre os desenvolvedores C?

Comentários

  • Apenas uma nota de observação, a maioria dos sistemas operacionais usa a opção 3 – o chamador aloca o buffer de qualquer maneira; informa o ponteiro do buffer e a capacidade; o receptor preenche o buffer er e também retorna o comprimento real da string se o buffer for insuficiente. Exemplo: sysctlbyname no OS X e iOS

Resposta

Os métodos que mais vi são 2 e 3.

O buffer fornecido pelo usuário é na verdade bastante simples de usar:

char[128] buffer; mytype_to_string(mt, buffer, 128); 

Embora a maioria das implementações retorne a quantidade de buffer usada.

A opção 2 será mais lenta e é perigosa ao usar bibliotecas vinculadas dinamicamente onde podem usar diferentes tempos de execução (e pilhas diferentes). Assim, você não pode liberar o que foi malocado em outra biblioteca. Isso requer uma função free_string(char*) para lidar com isso.

Comentários

  • Obrigado! Acho que também gosto mais da Alternativa 3. No entanto, quero ser capaz de fazer coisas como: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); e, portanto, não ‘ gostaria de retornar o comprimento usado, mas sim o ponteiro para a corda. O comentário da biblioteca dinâmica é realmente importante.
  • Deve ‘ ser sizeof(buffer) - 1 para satisfazer a \0 terminador?
  • @ Michael-O não o termo nulo é incluído no tamanho do buffer, o que significa que a string máxima que pode ser inserida é 1 menor do que o tamanho passado. Este é o padrão que a string segura funciona na biblioteca padrão como snprintf usa.
  • @ratchetfreak Obrigado pelo esclarecimento. Seria bom estender a resposta com essa sabedoria.

Resposta

Ideia adicional de design para # 3

Quando possível, forneça também o tamanho máximo necessário para mytype no mesmo arquivo .h que mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Agora o usuário pode codificar de acordo.

char buf[MYTYPE_TO_STRING_SIZE]; puts(mytype_to_string(mt, buf, sizeof buf)); 

Pedido

O tamanho dos arrays, quando primeiro, permite os tipos de VLA.

char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]); 

Não é tão importante com dimensão única, mas útil com 2 ou mais.

void matrix(size_t row, size_t col, double matrix[row][col]); 

Lembro-me de ter lido que ter o tamanho primeiro é um idioma preferido no próximo C. É preciso encontrar essa referência …

Resposta

Como complemento à excelente resposta de @ratchetfreak “, gostaria de salientar que a alternativa # 3 segue um paradigma / padrão semelhante às funções da biblioteca C padrão.

Por exemplo, strncpy.

 char * strncpy ( char * destination, const char * source, size_t num );  

Seguir o mesmo paradigma ajudaria para reduzir a carga cognitiva para novos desenvolvedores (ou mesmo seu futuro) quando eles precisam usar sua função.

A única diferença com o que você tem em sua postagem seria que o nas bibliotecas C tende a ser listado primeiro na lista de argumentos.Portanto:

 char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen );  

Resposta

I “d echo @ratchet_freak na maioria dos casos (talvez com um pequeno ajuste para sizeof buffer em 128) mas eu quero pular aqui com uma resposta esquisita. Que tal ser esquisito? Por que não, além dos problemas de receber olhares estranhos dos nossos colegas e ter que ser mais persuasivo? E eu ofereço isto:

// Note allocator parameter. char* mytype_to_string(allocator* alloc, const mytype_t* t) { char* buf = allocate(alloc, however_much_you_need); // fill out buf based on "t" contents return buf; } 

E exemplo de uso:

void func(my_type a, my_type b) { allocator alloc = allocator_new(); const char* str1 = mytype_to_string(&alloc, &a); if (!str1) goto oom; const char* str2 = mytype_to_string(&alloc, &b); if (!str2) goto oom // do something with str1 and str2 goto finish; oom: errno = ENOMEM; finish: // Frees all memory allocated through `alloc`. allocator_purge(&alloc); } 

Se você fez assim, pode tornar seu alocador muito eficiente (mais eficiente do que malloc em termos de custos de alocação / desalocação e também localidade de referência melhorada para acesso à memória). Pode ser um alocador de arena que envolve apenas incrementar um ponteiro em casos comuns para solicitações de alocação e pool de memória de forma sequencial a partir de grandes blocos contíguos (com o primeiro bloco nem mesmo exigindo um heap local – pode ser alocado na pilha). Ele simplifica o tratamento de erros. Além disso, e isso pode ser o mais discutível, mas acho que é praticamente óbvio em termos de como torna claro para o chamador que vai alocar memória para o alocador que você passar, exigindo liberação explícita (allocator_purge neste caso) sem ter que documentar tal comportamento em todas as funções possíveis se você usar este estilo de forma consistente. Esperançosamente, a aceitação do parâmetro de alocador o torna bastante óbvio.

Não sei. Recebo contra-argumentos aqui, como implementar o alocador de arena mais simples possível (apenas use o alinhamento máximo para todas as solicitações) e lidar com isso é muito trabalhoso. Meu pensamento direto é, o que somos nós, programadores de Python? Podemos usar Python se for o caso. Use Python se esses detalhes não importam. Estou falando sério. Tive muitos colegas de programação C que provavelmente escreveriam não apenas um código mais correto, mas possivelmente ainda mais eficiente com Python, uma vez que ignoram coisas como localidade de referência enquanto tropeçam em bugs que criam a torto e a direito. não vejo o que há de tão assustador em algum alocador de arena simples aqui se formos programadores C preocupados com coisas como localidade de dados e seleção de instrução ideal, e isso é indiscutivelmente muito menos para pensar do que pelo menos os tipos de interfaces que requerem chamadores para liberar explicitamente cada item individual que a interface pode retornar. Ele oferece desalocação em massa sobre desalocação individual de um tipo que é mais sujeito a erros. Um programador C adequado desafia chamadas loopy para malloc como eu vejo, especialmente quando ele aparece como um ponto de acesso em seu profiler. Do meu ponto de vista, deve haver mais ” oomph ” para uma justificativa para ainda ser um programador C em 2020, e nós podemos ” t evite coisas como alocadores de memória mais.

E isso não tem os casos extremos de alocar um buffer de tamanho fixo onde alocamos, digamos, 256 bytes e a string resultante é maior. Mesmo se nossas funções evitarem saturações de buffer (como com sprintf_s), há mais código para se recuperar adequadamente de tais erros que é necessário, o que podemos omitir com o caso de alocador, uma vez que não “não temos esses casos extremos. Não temos que lidar com esses casos no caso do alocador, a menos que realmente esgotemos o espaço de endereçamento físico de nosso hardware (que o código acima trata, mas não precisa lidar com ” sem buffer pré-alocado ” separadamente de ” sem memória “).

Resposta

Além do fato de que o que você está propondo fazer é um cheiro de código ruim, alternativa 3 soa melhor para mim. Também acho, como @ gnasher729, que você está usando a linguagem errada.

Comentários

Resposta

Para ser honesto, você pode querer mudar para um idioma diferente em que retornar uma string não seja uma operação complexa, trabalhosa e sujeita a erros.

Você pode considerar C ++ ou Objective-C onde você pode deixar 99% do seu código inalterado.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *