Initialiser plusieurs membres de classe constants à l'aide d'un seul appel de fonction C ++

50

Si j'ai deux variables de membres constants différentes, qui doivent toutes deux être initialisées en fonction du même appel de fonction, existe-t-il un moyen de le faire sans appeler la fonction deux fois?

Par exemple, une classe de fraction où le numérateur et le dénominateur sont constants.

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;
};

Cela se traduit par une perte de temps, car la fonction GCD est appelée deux fois. Vous pouvez également définir un nouveau membre de classe gcd_a_b, et affecter d'abord la sortie de gcd à celle de la liste d'initialisation, mais cela entraînerait un gaspillage de mémoire.

En général, existe-t-il un moyen de le faire sans appels de fonctions ou mémoire gaspillés? Pouvez-vous peut-être créer des variables temporaires dans une liste d'initialisation? Je vous remercie.

Qq0
la source
5
Avez-vous la preuve que "la fonction GCD est appelée deux fois"? Il est mentionné deux fois, mais ce n'est pas la même chose qu'un compilateur émettant du code qui l'appelle deux fois. Un compilateur peut déduire qu'il s'agit d'une fonction pure et réutiliser sa valeur à la deuxième mention.
Eric Towers
6
@EricTowers: Oui, les compilateurs peuvent parfois contourner le problème dans la pratique dans certains cas. Mais seulement s'ils peuvent voir la définition (ou une annotation dans un objet), sinon aucun moyen de prouver qu'il est pur. Vous devez compiler avec l' optimisation lien temps activé, mais pas tout le monde. Et la fonction pourrait être dans une bibliothèque. Ou prenons le cas d'une fonction qui n'ont des effets secondaires, et de l' appeler est exactement une fois une question de justesse?
Peter Cordes
@EricTowers Point intéressant. J'ai effectivement essayé de le vérifier en mettant une instruction print dans la fonction GCD, mais maintenant je me rends compte que cela l'empêcherait d'être une fonction pure.
Qq0
@ Qq0: Vous pouvez vérifier en regardant l'asm généré par le compilateur, par exemple en utilisant l'explorateur de compilateur Godbolt avec gcc ou clang -O3. Mais probablement pour toute implémentation de test simple, cela entraînerait en fait l'appel de fonction. Si vous utilisez __attribute__((const))ou pur sur le prototype sans fournir de définition visible, il devrait laisser GCC ou clang faire l'élimination de sous-expression commune (CSE) entre les deux appels avec le même argument. Notez que la réponse de Drew fonctionne même pour les fonctions non pures, c'est donc beaucoup mieux et vous devez l'utiliser à tout moment où la fonction pourrait ne pas être en ligne.
Peter Cordes
En règle générale, il vaut mieux éviter les variables de membre const non statiques. L'un des rares domaines où tout ne s'applique pas souvent. Par exemple, vous ne pouvez pas affecter d'objets de classe. Vous pouvez utiliser emplace_back dans un vecteur, mais seulement tant que la limite de capacité ne déclenche pas un redimensionnement.
doug

Réponses:

66

En général, existe-t-il un moyen de le faire sans appels de fonctions ni mémoire gaspillés?

Oui. Cela peut être fait avec un constructeur délégué , introduit en C ++ 11.

Un constructeur délégué est un moyen très efficace d'acquérir les valeurs temporaires nécessaires à la construction avant l' initialisation des variables membres.

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
la source
Par intérêt, les frais généraux d'appeler un autre constructeur seraient-ils importants?
Qq0
1
@ Qq0 Vous pouvez observer ici qu'il n'y a pas de surcharge avec de modestes optimisations activées.
Drew Dormann
2
@ Qq0: C ++ est conçu autour de compilateurs d'optimisation modernes. Ils peuvent insérer trivialement cette délégation, surtout si vous la rendez visible dans la définition de classe (dans le .h), même si la vraie définition du constructeur n'est pas visible pour l'inline. c'est-à-dire que l' gcd()appel serait inséré dans chaque site d'appel de constructeur et ne laisserait qu'un callau constructeur privé à 3 opérandes.
Peter Cordes
10

Les vars membres sont initialisés par l'ordre dans lequel ils sont déclarés dans la classe decleration, vous pouvez donc effectuer les opérations suivantes (mathématiquement)

#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;
}

Pas besoin d'appeler un autre constructeur ou même de le faire.

asmmo
la source
6
ok cela fonctionne spécifiquement pour GCD, mais de nombreux autres cas d'utilisation ne peuvent probablement pas dériver la 2e const des arguments et du premier. Et comme écrit, cela a une division supplémentaire qui est un autre inconvénient par rapport à l'idéal que le compilateur pourrait ne pas optimiser. GCD pourrait ne coûter qu'une division, ce qui pourrait être presque aussi mauvais que d'appeler GCD deux fois. (En supposant que la division domine le coût des autres opérations, comme c'est souvent le cas sur les processeurs modernes.)
Peter Cordes
@PeterCordes mais l'autre solution a un appel de fonction supplémentaire et alloue plus de mémoire d'instructions.
asmmo
1
Parlez-vous du constructeur délégué de Drew? Cela peut évidemment inclure la Fraction(a,b,gcd(a,b))délégation dans l'appelant, ce qui réduit le coût total. Cette intégration est plus facile à faire pour le compilateur que pour annuler la division supplémentaire. Je ne l'ai pas essayé sur godbolt.org mais vous pourriez si vous êtes curieux. Utilisez gcc ou clang -O3comme le ferait une construction normale. (C ++ est conçu autour de l'hypothèse d'un compilateur d'optimisation moderne, d'où des fonctionnalités comme constexpr)
Peter Cordes
-3

@Drew Dormann a donné une solution similaire à ce que j'avais en tête. Puisque OP ne mentionne jamais de ne pas pouvoir modifier le ctor, cela peut être appelé avec Fraction f {a, b, gcd(a, b)}:

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

Seulement de cette façon, il n'y a pas de second appel à une fonction, constructeur ou autre, donc ce n'est pas une perte de temps. Et ce n'est pas une mémoire perdue, car un temporaire devrait être créé de toute façon, vous pouvez donc aussi en faire bon usage. Cela évite également une division supplémentaire.

un citoyen concerné
la source
3
Votre modification ne permet même pas de répondre à la question. Maintenant, vous demandez à l'appelant de passer un 3e argument? Votre version d'origine utilisant l'affectation à l'intérieur du corps du constructeur ne fonctionne pas pour const, mais fonctionne au moins pour d'autres types. Et quelle division supplémentaire évitez-vous «également»? Vous voulez dire par rapport à la réponse de asmmo?
Peter Cordes
1
Ok, supprimé mon downvote maintenant que vous avez expliqué votre point. Mais cela semble assez évidemment terrible et vous oblige à intégrer manuellement une partie du travail du constructeur dans chaque appelant. C'est l'opposé de DRY (ne vous répétez pas) et de l'encapsulation des responsabilités / internes de la classe. La plupart des gens ne considéreraient pas cela comme une solution acceptable. Étant donné qu'il existe une façon C ++ 11 de le faire proprement, personne ne devrait jamais le faire à moins qu'il ne soit coincé avec une ancienne version C ++, et la classe a très peu d'appels à ce constructeur.
Peter Cordes
2
@aconcernedcitizen: Je ne veux pas dire pour des raisons de performances, je veux dire pour des raisons de qualité du code. À votre façon, si vous avez changé la façon dont cette classe fonctionnait en interne, vous devriez aller chercher tous les appels au constructeur et changer ce 3e argument. Ce supplément ,gcd(foo, bar)est un code supplémentaire qui pourrait et devrait donc être pris en compte dans chaque site d'appel de la source . C'est un problème de maintenabilité / lisibilité, pas de performances. Le compilateur l'inclura très probablement au moment de la compilation, ce que vous souhaitez pour les performances.
Peter Cordes
1
@PeterCordes Vous avez raison, maintenant je vois que mon esprit était fixé sur la solution, et j'ai ignoré tout le reste. Quoi qu'il en soit, la réponse reste, ne serait-ce que pour la honte. Chaque fois que j'aurai des doutes à ce sujet, je saurai où chercher.
un citoyen inquiet
1
Considérez également le cas de Fraction f( x+y, a+b ); Pour l'écrire à votre façon, vous devez écrire BadFraction f( x+y, a+b, gcd(x+y, a+b) );ou utiliser des vars tmp. Ou pire encore, que se passe-t-il si vous voulez écrire Fraction f( foo(x), bar(y) );- alors vous auriez besoin que le site d'appel déclare des vars tmp pour contenir les valeurs de retour, ou appelez à nouveau ces fonctions et espère que le compilateur les supprime, ce que nous évitons. Voulez-vous déboguer le cas d'un appelant mélangeant les arguments pour gcdque ce ne soit pas réellement le GCD des 2 premiers arguments passés au constructeur? Non? Alors ne rendez pas ce bug possible.
Peter Cordes