Manera idiomática de distinguir dos constructores de cero arg


41

Tengo una clase como esta:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Por lo general, quiero predeterminar (cero) inicializar la countsmatriz como se muestra.

Sin embargo, en las ubicaciones seleccionadas identificadas por la creación de perfiles, me gustaría suprimir la inicialización de la matriz, porque sé que la matriz está a punto de sobrescribirse, pero el compilador no es lo suficientemente inteligente como para descubrirlo.

¿Cuál es una forma idiomática y eficiente de crear un constructor de "cero argumentos" secundario?

Actualmente, estoy usando una clase de etiqueta uninit_tagque se pasa como un argumento ficticio, así:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Luego llamo al constructor no-init como event_counts c(uninit_tag{});cuando quiero suprimir la construcción.

Estoy abierto a soluciones que no implican la creación de una clase ficticia, o que son más eficientes de alguna manera, etc.


"porque sé que la matriz está a punto de sobrescribirse" ¿Estás 100% seguro de que tu compilador ya no está haciendo esa optimización? ejemplo: gcc.godbolt.org/z/bJnAuJ
Frank

66
@Frank - ¿Siento que la respuesta a su pregunta está en la segunda mitad de la oración que citó? No pertenece a la pregunta, pero pueden ocurrir una variedad de cosas: (a) a menudo el compilador simplemente no es lo suficientemente fuerte como para eliminar los almacenes muertos (b) a veces solo se sobrescribe un subconjunto de los elementos y esto derrota el optimización (pero solo ese mismo subconjunto se lee más tarde) (c) a veces el compilador podría hacerlo, pero es derrotado, por ejemplo, porque el método no está en línea.
BeeOnRope

¿Tienes otros constructores en tu clase?
NathanOliver

1
@Frank - ¿eh, tu caso muestra que gcc no elimina las tiendas muertas? De hecho, si me hubieras hecho adivinar, habría pensado que gcc resolvería este caso muy simple, pero si falla aquí, ¡imagina un caso un poco más complicado!
BeeOnRope

1
@uneven_mark: sí, gcc 9.2 lo hace a -O3 (pero esta optimización es poco común en comparación con -O2, IME), pero las versiones anteriores no. En general, la eliminación de las tiendas muertas es una cosa, pero es muy frágil y está sujeta a todas las advertencias habituales, como que el compilador pueda ver las tiendas muertas al mismo tiempo que ve las tiendas dominantes. Mi comentario fue más para aclarar lo que Frank estaba tratando de decir porque dijo "caso en cuestión: (enlace de godbolt)", pero el enlace muestra que ambas tiendas se están realizando (así que tal vez me estoy perdiendo algo).
BeeOnRope

Respuestas:


33

La solución que ya tiene es correcta, y es exactamente lo que me gustaría ver si estuviera revisando su código. Es lo más eficiente posible, claro y conciso.


1
El problema principal que tengo es si debo declarar un nuevo uninit_tagsabor en cada lugar en el que quiero usar este idioma. Esperaba que ya hubiera algo así como un tipo de indicador, tal vez en std::.
BeeOnRope

99
No hay una elección obvia de la biblioteca estándar. No definiría una nueva etiqueta para cada clase donde quisiera esta característica; definiría una etiqueta para todo el proyecto no_inity la usaría en todas mis clases donde sea necesaria.
John Zwinck

2
Creo que la biblioteca estándar tiene etiquetas varoniles para diferenciar iteradores y esas cosas y los dos std::piecewise_construct_ty std::in_place_t. Ninguno de ellos parece razonable de usar aquí. Tal vez le gustaría definir un objeto global de su tipo para usar siempre, por lo que no necesita las llaves en cada llamada de constructor. El STL hace esto con std::piecewise_constructpara std::piecewise_construct_t.
n314159

No es lo más eficiente posible. En la convención de llamadas AArch64, por ejemplo, la etiqueta tiene que estar asignada en pila, con efectos secundarios (no se puede llamar tampoco ...): godbolt.org/z/6mSsmq
TLW

1
@TLW Una vez que agregue el cuerpo a los constructores, no hay asignación de pila, godbolt.org/z/vkCD65
R2RT el

8

Si el cuerpo del constructor está vacío, se puede omitir o omitir:

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

Luego, la inicialización predeterminada event_counts counts; dejará sin counts.countsinicializar (la inicialización predeterminada no es operativa aquí), y la inicialización de event_counts counts{}; valor valorará la inicialización counts.counts, llenándola efectivamente con ceros.


3
Pero, de nuevo, debe recordar utilizar la inicialización de valor y OP quiere que sea seguro de forma predeterminada.
doc

@doc, estoy de acuerdo. Esta no es la solución exacta para lo que OP quiere. Pero esta inicialización imita los tipos incorporados. Porque int i;aceptamos que no tiene inicialización cero. Tal vez también deberíamos aceptar que event_counts counts;no está inicializado en cero y hacer event_counts counts{};nuestro nuevo valor predeterminado.
Evg

6

Me gusta tu solución También podría haber considerado la estructura anidada y la variable estática. Por ejemplo:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};

Con la variable estática, la llamada del constructor no inicializado puede parecer más conveniente:

event_counts e(event_counts::uninit);

Por supuesto, puede introducir una macro para guardar la escritura y convertirla en una característica más sistemática

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}

3

Creo que una enumeración es una mejor opción que una clase de etiqueta o un bool. No necesita pasar una instancia de una estructura y la persona que llama sabe claramente qué opción está obteniendo.

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

Luego, crear instancias se ve así:

event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

O, para que se parezca más al enfoque de la clase de etiqueta, use una enumeración de un solo valor en lugar de la clase de etiqueta:

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};

Entonces solo hay dos formas de crear una instancia:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};

Estoy de acuerdo contigo: la enumeración es más simple. Pero tal vez olvidaste esta línea:event_counts() : counts{} {}
azulada

@bluish, mi intención no era inicializar countsincondicionalmente, sino solo cuando INITestá configurado.
TimK

@bluish Creo que la razón principal para elegir una clase de etiqueta no es lograr simplicidad, sino señalar que el objeto no inicializado es especial, es decir, utiliza la función de optimización en lugar de la parte normal de la interfaz de clase. Ambos booly enumson decentes, pero debemos ser conscientes de que el uso de parámetros en lugar de sobrecarga tiene un tono semántico algo diferente. En el primero, claramente parametriza un objeto, por lo tanto, la posición inicializada / no inicializada se convierte en su estado, mientras que pasar un objeto de etiqueta a ctor es más como pedirle a la clase que realice una conversión. Por lo tanto, no es IMO una cuestión de elección sintáctica.
doc

@TimK Pero el OP quiere que el comportamiento predeterminado sea la inicialización de la matriz, por lo que creo que su solución a la pregunta debería incluir event_counts() : counts{} {}.
azulado

@bluish En mi sugerencia original countsse inicializa a std::fillmenos que NO_INITse solicite. Agregar el constructor predeterminado como usted sugiere haría dos formas diferentes de hacer la inicialización predeterminada, lo cual no es una gran idea. He agregado otro enfoque que evita el uso std::fill.
TimK

1

Es posible que desee considerar una inicialización de dos fases para su clase:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

El constructor anterior no inicializa la matriz a cero. Para establecer los elementos de la matriz en cero, debe llamar a la función miembro set_zero()después de la construcción.


77
Gracias, consideré este enfoque, pero quiero algo que mantenga seguro el valor predeterminado, es decir, cero por defecto, y solo en unos pocos lugares seleccionados anulo el comportamiento del inseguro.
BeeOnRope

3
Esto requerirá un cuidado adicional, pero los usos que se supone que no se han inicializado. Por lo tanto, es una fuente adicional de errores en relación con la solución de OP.
nuez

@BeeOnRope también se podría proporcionar std::functioncomo argumento de constructor con algo similar al set_zeroargumento predeterminado. Luego pasaría una función lambda si desea una matriz no inicializada.
doc

1

Lo haría así:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

El compilador será lo suficientemente inteligente como para omitir todo el código cuando lo use event_counts(false), y podrá decir exactamente lo que quiere decir en lugar de hacer que la interfaz de su clase sea tan extraña.


8
Tiene razón sobre la eficiencia, pero los parámetros booleanos no hacen que el código del cliente sea legible. Cuando estás leyendo y ves la declaración event_counts(false), ¿qué significa eso? No tiene idea sin volver atrás y mirar el nombre del parámetro. Es mejor al menos usar una enumeración o, en este caso, una clase centinela / etiqueta como se muestra en la pregunta. Luego, obtienes una declaración más como event_counts(no_init), que es obvio para todos en su significado.
Cody Gray

Creo que esta también es una solución decente. Puede descartar ctor predeterminado y usar el valor predeterminado event_counts(bool initCountr = true).
doc

Además, ctor debe ser explícito.
doc

desafortunadamente, actualmente C ++ no admite parámetros con nombre, pero podemos usar boost::parametery solicitar event_counts(initCounts = false)legibilidad
phuclv

1
Curiosamente, @doc, en event_counts(bool initCounts = true)realidad es un constructor predeterminado, debido a que cada parámetro tiene un valor predeterminado. El requisito es que sea invocable sin especificar argumentos, event_counts ec;no le importa si no tiene parámetros o si usa valores predeterminados.
Justin Time - Restablece a Monica el

1

Usaría una subclase solo para ahorrar un poco de escritura:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};

Usted puede deshacerse de la clase simulado por el cambio del argumento del constructor no inicializar a boolo into algo así, ya que no tiene por qué ser mnemotécnica más.

También podría intercambiar la herencia y definirla events_count_no_initcon un constructor predeterminado como sugirió Evg en su respuesta, y luego events_countser la subclase:

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};

Esta es una idea interesante, pero también siento que introducir un nuevo tipo causará fricción. Por ejemplo, cuando realmente quiera un no inicializado event_counts, querré que sea de tipo event_count, no event_count_uninitialized, así que debería cortar directamente en la construcción event_counts c = event_counts_no_init{};, lo que creo que elimina la mayoría de los ahorros en escribir.
BeeOnRope

@BeeOnRope Bueno, para la mayoría de los propósitos, un event_count_uninitializedobjeto es un event_countobjeto. Ese es el punto de herencia, no son tipos completamente diferentes.
Ross Ridge

De acuerdo, pero el problema es con "para la mayoría de los propósitos". No son intercambiables - por ejemplo, si se intenta ver Asignar ecua ecfunciona, pero no el otro alrededor de camino. O si usa funciones de plantilla, son de diferentes tipos y terminan con diferentes instancias, incluso si el comportamiento termina siendo idéntico (y a veces no lo será, por ejemplo, con miembros de plantilla estáticos). Especialmente con el uso intensivo de autoesto definitivamente puede surgir y ser confuso: no quisiera que la forma en que se inicializó un objeto se refleje permanentemente en su tipo.
BeeOnRope
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.