Inicializar múltiples miembros de clase constante utilizando una función llamada C ++

50

Si tengo dos variables de miembros constantes diferentes, que deben inicializarse en función de la misma llamada a la función, ¿hay alguna manera de hacerlo sin llamar a la función dos veces?

Por ejemplo, una clase de fracción donde el numerador y el denominador son constantes.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

Esto da como resultado una pérdida de tiempo, ya que la función GCD se llama dos veces. También podría definir un nuevo miembro de clase gcd_a_b, y primero asignar la salida de gcd a eso en la lista de inicializadores, pero luego esto conduciría a un desperdicio de memoria.

En general, ¿hay alguna manera de hacer esto sin llamadas de función o memoria desperdiciadas? ¿Quizás puede crear variables temporales en una lista de inicializador? Gracias.

Qq0
fuente
55
¿Tiene pruebas de que "la función GCD se llama dos veces"? Se menciona dos veces, pero eso no es lo mismo que un compilador que emite código que lo llama dos veces. Un compilador puede deducir que es una función pura y reutilizar su valor en la segunda mención.
Eric Towers
66
@EricTowers: Sí, los compiladores a veces pueden solucionar el problema en la práctica en algunos casos. Pero solo si pueden ver la definición (o alguna anotación en un objeto), de lo contrario no hay forma de demostrar que es pura. Usted debe compilar con activar la optimización en tiempo de enlace, pero no todo el mundo lo hace. Y la función podría estar en una biblioteca. O consideremos el caso de una función que hace tener efectos secundarios, y decir que es exactamente una vez es una cuestión de corrección?
Peter Cordes
@EricTowers Punto interesante. Realmente intenté verificarlo poniendo una declaración de impresión dentro de la función GCD, pero ahora me doy cuenta de que eso evitaría que fuera una función pura.
Qq0
@ Qq0: Puede verificar mirando el compilador generado asm, por ejemplo, utilizando el explorador del compilador Godbolt con gcc o clang -O3. Pero probablemente para cualquier implementación de prueba simple, en realidad se alinearía la llamada a la función. Si usa __attribute__((const))o es puro en el prototipo sin proporcionar una definición visible, debería permitir que GCC o clang hagan la eliminación de subexpresión común (CSE) entre las dos llamadas con el mismo argumento. Tenga en cuenta que la respuesta de Drew funciona incluso para funciones no puras, por lo que es mucho mejor y debe usarla siempre que la función no esté en línea.
Peter Cordes
En general, es mejor evitar las variables miembro const no estáticas. Una de las pocas áreas donde todo no suele aplicarse. Por ejemplo, no puede asignar objetos de clase. Puede emplace_back en un vector, pero solo mientras el límite de capacidad no genere un cambio de tamaño.
doug

Respuestas:

66

En general, ¿hay alguna manera de hacer esto sin llamadas de función o memoria desperdiciadas?

Si. Esto se puede hacer con un constructor delegante , introducido en C ++ 11.

Un constructor delegar es una forma muy eficiente de adquirir valores temporales necesarios para la construcción antes de cualquier se inicializan las variables miembro.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};
Drew Dormann
fuente
Fuera de interés, ¿sería importante la sobrecarga de llamar a otro constructor?
Qq0
1
@ Qq0 Puede observar aquí que no hay sobrecarga con optimizaciones modestas habilitadas.
Drew Dormann
2
@ Qq0: C ++ está diseñado en torno a compiladores de optimización modernos. Pueden en línea trivialmente esta delegación, especialmente si la haces visible en la definición de clase (en el .h), incluso si la definición de constructor real no es visible para la inserción. es decir, la gcd()llamada se alinearía en cada sitio de llamada del constructor, y dejaría solo calla al constructor privado de 3 operandos.
Peter Cordes
10

Las variables miembro se inicializan por el orden en que se declaran en la declinación de la clase, por lo tanto, puede hacer lo siguiente (matemáticamente)

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

No es necesario llamar a otros constructores o incluso hacerlos.

asmmo
fuente
66
ok, eso funciona específicamente para GCD, pero muchos otros casos de uso probablemente no pueden derivar la segunda const de los argumentos y la primera. Y como está escrito, esto tiene una división adicional que es otra desventaja versus ideal que el compilador podría no optimizar. GCD solo puede costar una división, por lo que puede ser casi tan malo como llamar a GCD dos veces. (Suponiendo que la división domina el costo de otras operaciones, como sucede a menudo en las CPU modernas.)
Peter Cordes,
@PeterCordes pero la otra solución tiene una llamada de función adicional y asigna más memoria de instrucciones.
asmmo
1
¿Estás hablando del constructor delegante de Drew? Obviamente, eso puede incluir a la Fraction(a,b,gcd(a,b))delegación en la persona que llama, lo que lleva a un menor costo total. Esa alineación es más fácil para el compilador que deshacer la división adicional en esto. No lo probé en godbolt.org pero podrías hacerlo si tienes curiosidad. Use gcc o clang -O3como lo haría una construcción normal. (C ++ está diseñado en torno a la suposición de un compilador de optimización moderno, por lo tanto, características como constexpr)
Peter Cordes
-3

@Drew Dormann dio una solución similar a lo que tenía en mente. Como OP nunca menciona que no se puede modificar el ctor, esto se puede llamar con Fraction f {a, b, gcd(a, b)}:

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

Solo de esta manera no hay una segunda llamada a una función, constructor o de otra manera, por lo que no se pierde tiempo. Y no es un desperdicio de memoria ya que de todos modos tendría que crearse un temporal, por lo que también puede aprovecharlo. También evita una división extra.

un ciudadano preocupado
fuente
3
Su edición hace que ni siquiera responda la pregunta. ¿Ahora está requiriendo que la persona que llama pase un tercer argumento? Su versión original usando la asignación dentro del cuerpo del constructor no funciona const, pero al menos funciona para otros tipos. ¿Y qué división adicional estás "también" evitando? ¿Te refieres a la respuesta de asmmo?
Peter Cordes
1
Ok, eliminé mi voto negativo ahora que has explicado tu punto. Pero esto parece bastante terrible, y requiere que inserte manualmente parte del trabajo del constructor en cada persona que llama. Esto es lo opuesto a DRY (no se repita) y la encapsulación de la responsabilidad / aspectos internos de la clase. La mayoría de la gente no consideraría esto como una solución aceptable. Dado que hay una manera C ++ 11 de hacer esto limpiamente, nadie debería hacerlo a menos que tal vez estén atascados con una versión anterior de C ++, y la clase tenga muy pocas llamadas a este constructor.
Peter Cordes
2
@aconcernedcitizen: no me refiero a razones de rendimiento, me refiero a razones de calidad de código. A tu manera, si alguna vez cambiaste el funcionamiento interno de esta clase, tendrías que buscar todas las llamadas al constructor y cambiar ese tercer argumento. Ese extra ,gcd(foo, bar)es un código extra que podría y, por lo tanto, debería factorizarse de cada sitio de llamadas en la fuente . Ese es un problema de mantenibilidad / legibilidad, no de rendimiento. Lo más probable es que el compilador lo incorpore en el momento de la compilación, que desea para el rendimiento.
Peter Cordes
1
@PeterCordes Tienes razón, ahora veo que mi mente estaba fija en la solución, y no hice caso de todo lo demás. De cualquier manera, la respuesta se mantiene, aunque solo sea por vergüenza. Siempre que tenga dudas al respecto, sabré dónde buscar.
un ciudadano preocupado
1
Considere también el caso de Fraction f( x+y, a+b ); Para escribirlo a su manera, tendría que escribir BadFraction f( x+y, a+b, gcd(x+y, a+b) );o usar tmp vars. O peor aún, ¿qué pasa si desea escribir Fraction f( foo(x), bar(y) );? Entonces necesitaría que el sitio de la llamada declare algunos tmp vars para mantener los valores de retorno, o llame a esas funciones nuevamente y espere que el compilador las elimine, lo cual es lo que estamos evitando. ¿Desea depurar el caso de una persona que llama mezclando los argumentos para gcdque en realidad no sea el MCD de los primeros 2 argumentos pasados ​​al constructor? ¿No? Entonces no hagas posible ese error.
Peter Cordes