Conveniente inicialización de estructura C ++

141

Estoy tratando de encontrar una manera conveniente de inicializar las estructuras C ++ 'pod'. Ahora, considere la siguiente estructura:

struct FooBar {
  int foo;
  float bar;
};
// just to make all examples work in C and C++:
typedef struct FooBar FooBar;

Si quiero iniciar esto convenientemente en C (!), Simplemente podría escribir:

/* A */ FooBar fb = { .foo = 12, .bar = 3.4 }; // illegal C++, legal C

Tenga en cuenta que quiero evitar explícitamente la siguiente notación, porque me parece que me rompen el cuello si cambio algo en la estructura en el futuro:

/* B */ FooBar fb = { 12, 3.4 }; // legal C++, legal C, bad style?

Para lograr lo mismo (o al menos similar) en C ++ como en el /* A */ejemplo, tendría que implementar un constructor idiota:

FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) {}
// ->
/* C */ FooBar fb(12, 3.4);

Lo cual es bueno para hervir agua, pero no es adecuado para personas perezosas (la pereza es algo bueno, ¿verdad?). Además, es casi tan malo como el /* B */ejemplo, ya que no establece explícitamente qué valor va a qué miembro.

Entonces, mi pregunta es básicamente ¿cómo puedo lograr algo similar /* A */o mejor en C ++? Alternativamente, estaría de acuerdo con una explicación de por qué no debería querer hacer esto (es decir, por qué mi paradigma mental es malo).

EDITAR

Por conveniente , quiero decir también mantenible y no redundante .

máscara de bits
fuente
2
Creo que el ejemplo B es lo más cercano que vas a llegar.
Marlon
2
No veo cómo el ejemplo B es "mal estilo". Tiene sentido para mí, ya que está inicializando cada miembro a su vez con sus respectivos valores.
Mike Bailey
26
Mike, es un mal estilo porque no está claro qué valor va a qué miembro. Tienes que ir a ver la definición de la estructura y luego contar los miembros para encontrar el significado de cada valor.
jnnnnn
9
Además, si la definición de FooBar cambiara en el futuro, la inicialización podría romperse.
Edward Falk
Si la inicialización se vuelve larga y compleja, no se olvide del patrón de construcción
trineo el

Respuestas:

20

Las inicializaciones designadas serán compatibles con c ++ 2a, pero no tiene que esperar, ya que oficialmente son compatibles con GCC, Clang y MSVC.

#include <iostream>
#include <filesystem>

struct hello_world {
    const char* hello;
    const char* world;
};

int main () 
{
    hello_world hw = {
        .hello = "hello, ",
        .world = "world!"
    };

    std::cout << hw.hello << hw.world << std::endl;
    return 0;
}

GCC Demo MSVC Demo

ivaigult
fuente
Advertencia: tenga en cuenta que si agrega parámetros al final de la estructura más adelante, las antiguas inicializaciones se compilarán silenciosamente sin haberse inicializado.
Catskul
1
@Catskul No. Se inicializará con una lista de inicializadores vacía, lo que dará como resultado una inicialización con cero.
ivaigult
Tienes razón. Gracias. Debo aclarar que los parámetros restantes se inicializarán de manera silenciosa por defecto. Lo que quería decir era que cualquiera que espere que esto pueda ayudar a hacer cumplir la inicialización explícita completa de los tipos de POD se decepcionará.
Catskul
43

Como style Ano está permitido en C ++ y no quieres style B, ¿qué tal si usas style BX:

FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 };  // :)

Al menos ayuda en alguna medida.

iammilind
fuente
8
+1: en realidad no garantiza una inicialización correcta (desde el punto de vista del compilador) pero sí ayuda al lector ... aunque los comentarios deben mantenerse sincronizados.
Matthieu M.
18
El comentario no impide que se rompa la inicialización de la estructura si inserto un nuevo campo entre fooy baren el futuro. C todavía inicializaría los campos que queremos, pero C ++ no lo haría. Y este es el punto de la pregunta: cómo lograr el mismo resultado en C ++. Quiero decir, Python hace esto con argumentos con nombre, C - con campos "con nombre", y C ++ también debería tener algo, espero.
dmitry_romanov
2
¿Comentarios sincronizados? Dáme un respiro. La seguridad pasa por la ventana. Reordenar los parámetros y el boom. Mucho mejor con explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Tenga en cuenta la palabra clave explícita . Incluso romper el estándar es mejor en lo que respecta a la seguridad. En Clang: -Wno-c99-extensiones
Daniel O
@ Daniel Daniel, no se trata de qué es mejor o qué no. esta respuesta de acuerdo con que el OP no quiere Estilo A (no c ++), B o C, que cubre todos los casos válidos.
iammilind 01 de
@iammilind Creo que una pista de por qué el paradigma mental de OP es malo podría mejorar la respuesta. Considero esto peligroso como lo es ahora.
Daerst
10

Podrías usar una lambda:

const FooBar fb = [&] {
    FooBar fb;
    fb.foo = 12;
    fb.bar = 3.4;
    return fb;
}();

Se puede encontrar más información sobre este idioma en el blog de Herb Sutter .

pestaña
fuente
1
Tal enfoque inicializa los campos dos veces. Una vez en constructor. En segundo lugar es fb.XXX = YYY.
Dmytro Ovdiienko
9

Extraiga los contenidos en funciones que los describen (refactorización básica):

FooBar fb = { foo(), bar() };

Sé que el estilo es muy cercano al que no quería usar, pero permite reemplazar más fácilmente los valores constantes y también explicarlos (por lo tanto, no es necesario editar comentarios), si alguna vez cambian, eso es.

Otra cosa que podrías hacer (ya que eres flojo) es hacer que el constructor esté en línea, para que no tengas que escribir tanto (eliminando "Foobar ::" y el tiempo dedicado a cambiar entre el archivo h y cpp):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};
ralphtheninja
fuente
1
Recomiendo a cualquier persona que lea esta pregunta que elija el estilo en el fragmento de código inferior para esta respuesta si todo lo que está buscando es poder inicializar rápidamente estructuras con un conjunto de valores.
kayleeFrye_onDeck
8

Su pregunta es algo difícil porque incluso la función:

static FooBar MakeFooBar(int foo, float bar);

puede ser llamado como:

FooBar fb = MakeFooBar(3.4, 5);

debido a las reglas de promoción y conversiones para los tipos numéricos integrados. (C nunca ha sido muy fuertemente tipado)

En C ++, lo que quiere se puede lograr, aunque con la ayuda de plantillas y afirmaciones estáticas:

template <typename Integer, typename Real>
FooBar MakeFooBar(Integer foo, Real bar) {
  static_assert(std::is_same<Integer, int>::value, "foo should be of type int");
  static_assert(std::is_same<Real, float>::value, "bar should be of type float");
  return { foo, bar };
}

En C, puede nombrar los parámetros, pero nunca llegará más lejos.

Por otro lado, si todo lo que quieres son parámetros nombrados, entonces escribes mucho código engorroso:

struct FooBarMaker {
  FooBarMaker(int f): _f(f) {}
  FooBar Bar(float b) const { return FooBar(_f, b); }
  int _f;
};

static FooBarMaker Foo(int f) { return FooBarMaker(f); }

// Usage
FooBar fb = Foo(5).Bar(3.4);

Y puede agregarle protección de promoción tipo si lo desea.

Matthieu M.
fuente
1
"En C ++, lo que quiere se puede lograr": ¿OP no estaba pidiendo ayuda para evitar la confusión del orden de los parámetros? ¿Cómo lograría eso la plantilla que propones? Solo por simplicidad, digamos que tenemos 2 parámetros, ambos int.
max
@max: solo lo evitará si los tipos difieren (incluso si son convertibles entre sí), que es la situación OP. Si no puede distinguir los tipos, entonces, por supuesto, no funciona, pero esa es otra cuestión.
Matthieu M.
Ah lo tengo. Sí, estos son dos problemas diferentes, y supongo que el segundo no tiene una buena solución en C ++ en este momento (pero parece que C ++ 20 está agregando el soporte para los nombres de parámetros de estilo C99 en la inicialización agregada).
max
6

Las interfaces de C ++ de muchos compiladores (incluidos GCC y clang) entienden la sintaxis del inicializador de C. Si puedes, simplemente usa ese método.

Matthias Urlichs
fuente
16
¡Lo cual no cumple con el estándar C ++!
bitmask el
55
Sé que no es estándar. Pero si puede usarlo, sigue siendo la forma más sensata de inicializar una estructura.
Matthias Urlichs
2
Puede proteger los tipos de xey haciendo privado el constructor incorrecto:private: FooBar(float x, int y) {};
dmitry_romanov
44
clang (compilador de c ++ basado en llvm) también admite esta sintaxis. Lástima que no sea parte del estándar.
nimrodm
Todos sabemos que los inicializadores C no son parte del estándar C ++. Pero muchos compiladores lo entienden y la pregunta no decía a qué compilador se dirige, si es que hay alguno. Por lo tanto, no desestime esta respuesta.
Matthias Urlichs
4

Otra forma más en C ++ es

struct Point
{
private:

 int x;
 int y;

public:
    Point& setX(int xIn) { x = Xin; return *this;}
    Point& setY(int yIn) { y = Yin; return *this;}

}

Point pt;
pt.setX(20).setY(20);
parapura rajkumar
fuente
2
Resulta engorroso para la programación funcional (es decir, crear el objeto en la lista de argumentos de una llamada de función), ¡pero de lo contrario es una buena idea!
bitmask
27
el optimizador probablemente lo reduce, pero mis ojos no.
Matthieu M.
66
Dos palabras: argh ... ¡argh! ¿Cómo es esto mejor que usar datos públicos con 'Punto pt; pt.x = pt.y = 20; `? O si quieres encapsular, ¿cómo es esto mejor que un constructor?
OldPeculier
3
Es mejor que un constructor porque tienes que mirar la declaración del constructor para el orden de los parámetros ... es x, y o y, x pero la forma en que lo he mostrado es evidente en el sitio de la llamada
parapura rajkumar
2
Esto no funciona si desea una estructura constante. o si desea decirle al compilador que no permita estructuras no inicializadas. Si realmente quieres hacerlo de esta manera, ¡al menos marca los setters con inline!
Matthias Urlichs
3

Opción D:

FooBar FooBarMake(int foo, float bar)

Legal C, legal C ++. Fácilmente optimizable para POD. Por supuesto, no hay argumentos con nombre, pero esto es como todos los C ++. Si desea argumentos con nombre, el objetivo C debería ser una mejor opción.

Opción E:

FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

Legal C, legal C ++. Argumentos nombrados.

Juan
fuente
12
En lugar de memset que puede usar FooBar fb = {};en C ++, inicializa por defecto todos los miembros de la estructura.
Öö Tiib
@ ÖöTiib: Desafortunadamente, eso es ilegal C, sin embargo.
CB Bailey
3

Sé que esta pregunta es antigua, pero hay una manera de resolver esto hasta que C ++ 20 finalmente traiga esta característica de C a C ++. Lo que puede hacer para resolver esto es usar macros de preprocesador con static_asserts para verificar que su inicialización sea válida. (Sé que las macros son generalmente malas, pero aquí no veo otra forma). Vea el código de ejemplo a continuación:

#define INVALID_STRUCT_ERROR "Instantiation of struct failed: Type, order or number of attributes is wrong."

#define CREATE_STRUCT_1(type, identifier, m_1, p_1) \
{ p_1 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_2(type, identifier, m_1, p_1, m_2, p_2) \
{ p_1, p_2 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_4(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3, m_4, p_4) \
{ p_1, p_2, p_3, p_4 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_4) >= (offsetof(type, m_3) + sizeof(identifier.m_3)), INVALID_STRUCT_ERROR);\

// Create more macros for structs with more attributes...

Luego, cuando tenga una estructura con atributos const, puede hacer esto:

struct MyStruct
{
    const int attr1;
    const float attr2;
    const double attr3;
};

const MyStruct test = CREATE_STRUCT_3(MyStruct, test, attr1, 1, attr2, 2.f, attr3, 3.);

Es un poco incómodo, porque necesita macros para cada número posible de atributos y necesita repetir el tipo y el nombre de su instancia en la llamada de macro. Además, no puede usar la macro en una declaración de devolución, porque las afirmaciones vienen después de la inicialización.

Pero sí resuelve su problema: cuando cambia la estructura, la llamada fallará en tiempo de compilación.

Si usa C ++ 17, incluso puede hacer que estas macros sean más estrictas al forzar los mismos tipos, por ejemplo:

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_1) == typeid(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_2) == typeid(identifier.m_2), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_3) == typeid(identifier.m_3), INVALID_STRUCT_ERROR);\
Max Vollmer
fuente
¿Existe una propuesta de C ++ 20 para permitir los inicializadores con nombre?
Maël Nison
1
@ MaëlNison Sí: inicializadores designados (desde C ++ 20)
Max Vollmer
2

La forma /* B */está bien en C ++, también C ++ 0x va a extender la sintaxis, por lo que también es útil para contenedores C ++. No entiendo por qué lo llamas mal estilo?

Si desea indicar parámetros con nombres, puede usar la biblioteca de parámetros de impulso , pero puede confundir a alguien que no esté familiarizado con ellos.

Reordenar los miembros de la estructura es como reordenar los parámetros de la función, tal refactorización puede causar problemas si no lo hace con mucho cuidado.

Öö Tiib
fuente
77
Lo llamo mal estilo porque creo que es cero mantenible. ¿Qué pasa si agrego otro miembro en un año? ¿O si cambio los pedidos / tipos de los miembros? Cada pieza de código que lo inicializa podría (muy probablemente) romperse.
máscara de bits
2
@bitmask Pero siempre y cuando no tenga argumentos con nombre, también debería actualizar las llamadas de constructor y creo que no mucha gente piensa que los constructores son un mal estilo inmanejable. También creo que la inicialización con nombre no es C, sino C99, de la cual C ++ definitivamente no es un superconjunto.
Christian Rau
2
Si agrega otro miembro en un año al final de la estructura, se inicializará por defecto en el código ya existente. Si los reordena, debe editar todo el código existente, nada que hacer.
Öö Tiib
1
@bitmask: El primer ejemplo también sería "imposible de mantener". ¿Qué sucede si cambias el nombre de una variable en la estructura? Claro, podría hacer un reemplazo total, pero eso podría cambiar accidentalmente el nombre de una variable que no debería cambiarse.
Mike Bailey
@ChristianRau ¿Desde cuándo C99 no es C? ¿No es C el grupo y C99 una versión particular / especificación ISO?
altendky
1

¿Qué pasa con esta sintaxis?

typedef struct
{
    int a;
    short b;
}
ABCD;

ABCD abc = { abc.a = 5, abc.b = 7 };

Acabo de probar en un Microsoft Visual C ++ 2015 y en g ++ 6.0.2. Trabajando bien
También puede hacer una macro específica si desea evitar duplicar el nombre de la variable.

cls
fuente
clang++3.5.0-10 con -Weverything -std=c++1zparece confirmar eso. Pero no se ve bien. ¿Sabes dónde el estándar confirma que esto es válido C ++?
máscara de bits
No lo sé, pero lo he usado en diferentes compiladores desde hace mucho tiempo y no vi ningún problema. Ahora probado en g ++ 4.4.7 - funciona bien.
cls
55
No creo que esto funcione. Tratar ABCD abc = { abc.b = 7, abc.a = 5 };.
raymai97
@deselect, funciona porque el campo se inicializa con valor, devuelto por operator =. Entonces, en realidad inicializas a un miembro de la clase dos veces.
Dmytro Ovdiienko
1

Para mí, la forma más perezosa de permitir la inicialización en línea es usar esta macro.

#define METHOD_MEMBER(TYPE, NAME, CLASS) \
CLASS &set_ ## NAME(const TYPE &_val) { NAME = _val; return *this; } \
TYPE NAME;

struct foo {
    METHOD_MEMBER(string, attr1, foo)
    METHOD_MEMBER(int, attr2, foo)
    METHOD_MEMBER(double, attr3, foo)
};

// inline usage
foo test = foo().set_attr1("hi").set_attr2(22).set_attr3(3.14);

Esa macro crea atributo y método de autorreferencia.

Emanuele Pavanello
fuente