Valores padrão em uma estrutura C

92

Eu tenho uma estrutura de dados como esta:

    struct foo {
        int id;
        rota interna;
        int backup_route;
        int current_route;
    }

e uma função chamada update () que é usada para solicitar mudanças nele.

  atualização (42, dont_care, dont_care, new_route);

isso é muito longo e se eu adicionar algo à estrutura eu tenho que adicionar um 'dont_care' para CADA chamada para atualizar (...).

Estou pensando em passar uma estrutura para ela, mas preencher a estrutura com 'dont_care' antecipadamente é ainda mais tedioso do que apenas soletrá-la na chamada de função. Posso criar a estrutura em algum lugar com valores padrão de dont care e apenas definir os campos que me interessam depois de declará-lo como uma variável local?

    struct foo bar = {.id = 42, .current_route = new_route};
    atualizar (& bar);

Qual é a maneira mais elegante de passar apenas as informações que desejo expressar para a função de atualização?

e eu quero que todo o resto seja padronizado como -1 (o código secreto para 'não me importo')

Arthur Ulfeldt
fonte

Respostas:

155

Embora macros e / ou funções (como já sugerido) funcionem (e possam ter outros efeitos positivos (por exemplo, ganchos de depuração)), eles são mais complexos do que o necessário. A solução mais simples e possivelmente mais elegante é apenas definir uma constante que você usa para a inicialização da variável:

const struct foo FOO_DONT_CARE = { // or maybe FOO_DEFAULT or something
    dont_care, dont_care, dont_care, dont_care
};
...
struct foo bar = FOO_DONT_CARE;
bar.id = 42;
bar.current_route = new_route;
update(&bar);

Este código não tem virtualmente nenhuma sobrecarga mental de compreensão da indireção, e é muito claro quais campos barvocê define explicitamente enquanto ignora (com segurança) aqueles que você não define.

Hlovdal
fonte
6
Outro bônus dessa abordagem é que ela não depende dos recursos do C99 para funcionar.
D.Shawley de
24
Quando mudei para 500 linhas "caí fora" do projeto. Gostaria de poder votar duas vezes neste!
Arthur Ulfeldt
4
PTHREAD_MUTEX_INITIALIZERusa isso também.
yingted de
Eu adicionei uma resposta abaixo contando com X-Macros para ser flexível no número de elementos presentes na estrutura.
Antonio
15

Você pode alterar seu valor especial secreto para 0 e explorar a semântica de membro da estrutura padrão do C

struct foo bar = { .id = 42, .current_route = new_route };
update(&bar);

irá então passar 0 como membros da barra não especificada no inicializador.

Ou você pode criar uma macro que fará a inicialização padrão para você:

#define FOO_INIT(...) { .id = -1, .current_route = -1, .quux = -1, ## __VA_ARGS__ }

struct foo bar = FOO_INIT( .id = 42, .current_route = new_route );
update(&bar);
jpalecek
fonte
FOO_INIT funciona? O compilador deve reclamar, eu acho, se você inicializar o mesmo membro duas vezes.
Jonathan Leffler,
1
Eu tentei com o gcc e não reclamou. Além disso, não encontrei nada contra isso no padrão, na verdade, há um exemplo em que a substituição é mencionada especificamente.
jpalecek de
2
C99: open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf : 6.7.8.19: A inicialização deve ocorrer na ordem da lista de inicializadores, cada inicializador fornecido para um subobjeto particular substituindo qualquer inicializador listado anteriormente para o mesmo subobjeto;
altendky
2
Se isso for verdade, você não poderia definir DEFAULT_FOO, que tem todos os inicializadores, e então fazer struct foo bar = {DEFAULT_FOO, .id = 10}; ?
João
7

<stdarg.h>permite que você defina funções variáveis ​​(que aceitam um número indefinido de argumentos, como printf()). Eu definiria uma função que recebesse um número arbitrário de pares de argumentos, um que especifica a propriedade a ser atualizada e outro que especifica o valor. Use um enumou uma string para especificar o nome da propriedade.

Matt J
fonte
Olá @Matt, como mapear enumvariáveis ​​para o structcampo? Hardcode it? Então esta função será aplicável apenas a específicos struct.
misssprite de
5

Em vez disso, considere usar uma definição de macro de pré-processador:

#define UPDATE_ID(instance, id)  ({ (instance)->id= (id); })
#define UPDATE_ROUTE(instance, route)  ({ (instance)->route = (route); })
#define UPDATE_BACKUP_ROUTE(instance, route)  ({ (instance)->backup_route = (route); })
#define UPDATE_CURRENT_ROUTE(instance, route)  ({ (instance)->current_route = (route); })

Se sua instância de (struct foo) for global, você não precisa do parâmetro para isso, é claro. Mas estou supondo que você provavelmente tem mais de uma instância. Usar o bloco ({...}) é um GNU-ismo que se aplica ao GCC; é uma maneira agradável (segura) de manter as linhas juntas como um bloco. Se, posteriormente, você precisar adicionar mais macros, como verificação de validação de intervalo, não terá que se preocupar em quebrar coisas como instruções if / else e assim por diante.

Isso é o que eu faria, com base nos requisitos que você indicou. Situações como essa são uma das razões pelas quais comecei a usar muito o python; manipular parâmetros padrão e tal se torna muito mais simples do que nunca com C. (Eu acho que é um plug Python, desculpe ;-)

digijock
fonte
4

Um padrão que o gobject usa é uma função variada e valores enumerados para cada propriedade. A interface é semelhante a:

update (ID, 1,
        BACKUP_ROUTE, 4,
        -1); /* -1 terminates the parameter list */

Escrever uma função varargs é fácil - consulte http://www.eskimo.com/~scs/cclass/int/sx11b.html . Basta combinar pares chave -> valor e definir os atributos de estrutura apropriados.

John Millikin
fonte
4

Visto que parece que você só precisa dessa estrutura para a update()função, não use uma estrutura para isso, ela apenas ofuscará sua intenção por trás dessa construção. Talvez você deva repensar porque está mudando e atualizando esses campos e definir funções ou macros separadas para essas "pequenas" mudanças.

por exemplo


#define set_current_route(id, route) update(id, dont_care, dont_care, route)
#define set_route(id, route) update(id, dont_care, route, dont_care)
#define set_backup_route(id, route) update(id, route, dont_care, dont_care)

Ou, melhor ainda, escreva uma função para cada caso de alteração. Como você já notou, você não altera todas as propriedades ao mesmo tempo, portanto, torne possível alterar apenas uma propriedade por vez. Isso não só melhora a legibilidade, mas também ajuda a lidar com os diferentes casos, por exemplo, você não precisa verificar todos os "dont_care" porque você sabe que apenas a rota atual é alterada.

quinmars
fonte
alguns casos irão alterar 3 deles na mesma chamada.
Arthur Ulfeldt,
Você pode ter esta sequência: set_current_rout (id, rota); set_route (id, rota); apply_change (id); ou similar. O que eu acho é que você pode ter uma chamada de função para cada setter. E faça o cálculo real (ou o que quer que seja) em um ponto posterior.
quinmars,
4

Que tal algo como:

struct foo bar;
update(init_id(42, init_dont_care(&bar)));

com:

struct foo* init_dont_care(struct foo* bar) {
  bar->id = dont_care;
  bar->route = dont_care;
  bar->backup_route = dont_care;
  bar->current_route = dont_care;
  return bar;
}

e:

struct foo* init_id(int id, struct foo* bar) {
  bar->id = id;
  return bar;
}

e correspondentemente:

struct foo* init_route(int route, struct foo* bar);
struct foo* init_backup_route(int backup_route, struct foo* bar);
struct foo* init_current_route(int current_route, struct foo* bar);

Em C ++, um padrão semelhante tem um nome que não me lembro agora.

EDIT : É chamado de Named Parameter Idiom .

Reunanen
fonte
Eu acredito que seja chamado de construtor.
Desconhecido em
Não, eu quis dizer o que você pode fazer: update (bar.init_id (42) .init_route (5) .init_backup_route (66));
Reunanen,
Parece ser chamado de "inicialização estilo Smalltalk encadeada", pelo menos aqui: cct.lsu.edu/~rguidry/ecl31docs/api/org/eclipse/ui/internal/…
Reunanen
2

Estou enferrujado com estruturas, então provavelmente estou faltando algumas palavras-chave aqui. Mas por que não começar com uma estrutura global com os padrões inicializados, copiá-la para sua variável local e depois modificá-la?

Um inicializador como:

void init_struct( structType * s )
{
   memcopy(s,&defaultValues,sizeof(structType));
}

Então, quando você quiser usá-lo:

structType foo;
init_struct( &foo ); // get defaults
foo.fieldICareAbout = 1; // modify fields
update( &foo ); // pass to function
Steven Fisher
fonte
2

Você pode resolver o problema com um X-Macro

Você mudaria sua definição de estrutura para:

#define LIST_OF_foo_MEMBERS \
    X(int,id) \
    X(int,route) \
    X(int,backup_route) \
    X(int,current_route)


#define X(type,name) type name;
struct foo {
    LIST_OF_foo_MEMBERS 
};
#undef X

E então você seria capaz de definir facilmente uma função flexível que define todos os campos como dont_care.

#define X(type,name) in->name = dont_care;    
void setFooToDontCare(struct foo* in) {
    LIST_OF_foo_MEMBERS 
}
#undef X

Seguindo a discussão aqui , também se pode definir um valor padrão desta forma:

#define X(name) dont_care,
const struct foo foo_DONT_CARE = { LIST_OF_STRUCT_MEMBERS_foo };
#undef X

O que se traduz em:

const struct foo foo_DONT_CARE = {dont_care, dont_care, dont_care, dont_care,};

E use como na resposta hlovdal , com a vantagem de que aqui a manutenção é mais fácil, ou seja, a alteração do número de membros da estrutura será atualizada automaticamentefoo_DONT_CARE . Observe que a última vírgula "espúria" é aceitável .

Eu aprendi o conceito de X-Macros pela primeira vez quando tive que resolver esse problema .

É extremamente flexível para novos campos sendo adicionados à estrutura. Se você tiver diferentes tipos de dados, poderá definir dont_carevalores diferentes dependendo do tipo de dados: a partir daqui , você pode se inspirar na função usada para imprimir os valores no segundo exemplo.

Se estiver tudo bem com uma intestrutura completa, você pode omitir o tipo de dados de LIST_OF_foo_MEMBERSe simplesmente alterar a função X da definição da estrutura para#define X(name) int name;

Antonio
fonte
Esta técnica explora que o pré-processador C usa ligação tardia e isso pode ser muito útil quando você quer um único lugar para definir algo (por exemplo, estados) e então ter 100% de certeza de que em todos os lugares quando os itens usados ​​estão sempre na mesma ordem, novos itens são automaticamente adicionados e excluídos os itens removidos. Não gosto muito de usar nomes curtos como Xe geralmente acrescentam _ENTRYao nome da lista, por exemplo #define LIST_SOMETHING LIST_SOMETHING_ENTRY(a1, a2, aN) \ LIST_SOMETHING_ENTRY(b1, b2, bN) ....
hlovdal
1

A maneira mais elegante seria atualizar os campos de estrutura diretamente, sem ter que usar a update()função - mas talvez haja boas razões para usá-la que não aparecem na pergunta.

struct foo* bar = get_foo_ptr();
foo_ref.id = 42;
foo_ref.current_route = new_route;

Ou você pode, como sugeriu Pukku, criar funções de acesso separadas para cada campo da estrutura.

Caso contrário, a melhor solução que posso pensar é tratar um valor de '0' em um campo de estrutura como um sinalizador 'não atualizar' - então, basta criar uma função para retornar uma estrutura zerada e, em seguida, usar isso para atualizar.

struct foo empty_foo(void)
{
    struct foo bar;
    bzero(&bar, sizeof (struct bar));
    return bar;    
}

struct foo bar = empty_foo();
bar.id=42;
bar.current_route = new_route;
update(&bar);

No entanto, isso pode não ser muito viável, se 0 for um valor válido para campos na estrutura.

gnud
fonte
Uma rede é um bom motivo =) Mas se -1 for o valor 'não se preocupe', então apenas restaure a função empty_foo () e use essa abordagem?
gnud
desculpe, eu cliquei no downvote por engano e só percebi depois !!! Não consigo encontrar nenhuma maneira de desfazer isso agora, a menos que haja uma edição ...
Ciro Santilli 郝海东 冠状 病 六四 事件 事件 法轮功