Façon idiomatique de distinguer deux constructeurs sans arg

41

J'ai une classe comme ça:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Habituellement, je veux initialiser par défaut (zéro) le countstableau comme indiqué.

À des emplacements sélectionnés identifiés par le profilage, cependant, je voudrais supprimer l'initialisation du tableau, car je sais que le tableau est sur le point d'être écrasé, mais le compilateur n'est pas assez intelligent pour le comprendre.

Quelle est une manière idiomatique et efficace de créer un constructeur zéro-arg «secondaire»?

Actuellement, j'utilise une classe de balises uninit_tagqui est passée comme argument factice, comme ceci:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Ensuite, j'appelle le constructeur no-init comme event_counts c(uninit_tag{});quand je veux supprimer la construction.

Je suis ouvert aux solutions qui n'impliquent pas la création d'une classe factice, ou qui sont plus efficaces d'une manière ou d'une autre, etc.

BeeOnRope
la source
"parce que je sais que le tableau est sur le point d'être écrasé" Êtes-vous sûr à 100% que votre compilateur ne fait pas déjà cette optimisation pour vous? cas d'espèce: gcc.godbolt.org/z/bJnAuJ
Frank
6
@Frank - J'ai l'impression que la réponse à votre question se trouve dans la seconde moitié de la phrase que vous avez citée? Cela n'appartient pas à la question, mais diverses choses peuvent se produire: (a) souvent, le compilateur n'est tout simplement pas assez fort pour éliminer les magasins morts (b) parfois, seul un sous-ensemble des éléments est écrasé, ce qui vainc le optimisation (mais seul ce même sous-ensemble est lu plus tard) (c) parfois le compilateur pourrait le faire, mais est vaincu par exemple, parce que la méthode n'est pas intégrée.
BeeOnRope
Avez-vous d'autres constructeurs dans votre classe?
NathanOliver
1
@Frank - eh, votre exemple montre que gcc n'élimine pas les magasins morts? En fait, si vous m'aviez fait deviner, j'aurais pensé que gcc obtiendrait ce cas très simple, mais s'il échoue ici, imaginez un cas un peu plus compliqué!
BeeOnRope
1
@uneven_mark - oui, gcc 9.2 le fait à -O3 (mais cette optimisation est rare par rapport à -O2, IME), mais pas les versions antérieures. En général, l'élimination des magasins morts est une chose, mais elle est très fragile et soumise à toutes les mises en garde habituelles, telles que le compilateur pouvant voir les magasins morts en même temps qu'il voit les magasins dominants. Mon commentaire était plus de clarifier ce que Frank essayait de dire parce qu'il a dit "cas d'espèce: (lien Godbolt)" mais le lien montre les deux magasins en cours d'exécution (alors peut-être que je manque quelque chose).
BeeOnRope

Réponses:

33

La solution que vous avez déjà est correcte, et c'est exactement ce que je voudrais voir si je révisais votre code. Il est aussi efficace que possible, clair et concis.

John Zwinck
la source
1
Le principal problème que j'ai est de savoir si je dois déclarer une nouvelle uninit_tagsaveur à chaque endroit où je veux utiliser cet idiome. J'espérais qu'il y avait déjà quelque chose comme un tel type d'indicateur, peut-être dans std::.
BeeOnRope
9
Il n'y a pas de choix évident dans la bibliothèque standard. Je ne définirais pas une nouvelle balise pour chaque classe où je veux cette fonctionnalité - je définirais une no_initbalise à l' échelle du projet et l'utiliserais dans toutes mes classes là où c'est nécessaire.
John Zwinck
2
Je pense que la bibliothèque standard a des balises viriles pour différencier les itérateurs et ces trucs et les deux std::piecewise_construct_tet std::in_place_t. Aucun d'entre eux ne semble raisonnable à utiliser ici. Vous souhaitez peut-être définir un objet global de votre type à utiliser toujours, de sorte que vous n'ayez pas besoin des accolades dans chaque appel de constructeur. La STL fait cela avec std::piecewise_constructpour std::piecewise_construct_t.
n314159
Ce n'est pas aussi efficace que possible. Dans la convention d' appel AArch64 par exemple l'étiquette doit être pile alloué, avec effets induits (ne peut pas appel de queue non plus ...): godbolt.org/z/6mSsmq
BLT
1
@TLW Une fois que vous ajoutez le corps aux constructeurs, il n'y a pas d'allocation de pile, godbolt.org/z/vkCD65
R2RT
8

Si le corps du constructeur est vide, il peut être omis ou par défaut:

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

Ensuite, l' initialisation par défaut event_counts counts; ne sera pas counts.countsinitialisée (l'initialisation par défaut est un no-op ici), et l' initialisation de la event_counts counts{}; valeur donnera la valeur initialize counts.counts, la remplissant efficacement avec des zéros.

Evg
la source
3
Mais là encore, vous devez vous rappeler d'utiliser l'initialisation de la valeur et OP veut que ce soit sûr par défaut.
doc
@doc, je suis d'accord. Ce n'est pas la solution exacte pour ce que veut OP. Mais cette initialisation imite les types intégrés. Car int i;nous acceptons qu'il n'est pas initialisé à zéro. Peut-être devrions-nous également accepter que ce event_counts counts;ne soit pas initialisé à zéro et définir event_counts counts{};notre nouveau paramètre par défaut.
Evg
6

J'aime ta solution. Vous pourriez également avoir considéré la structure imbriquée et la variable statique. Par exemple:

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

};

Avec une variable statique, l'appel du constructeur non initialisé peut sembler plus pratique:

event_counts e(event_counts::uninit);

Vous pouvez bien sûr introduire une macro pour enregistrer la saisie et en faire une fonctionnalité plus systématique

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

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}
doc
la source
3

Je pense qu'une énumération est un meilleur choix qu'une classe de balises ou un booléen. Vous n'avez pas besoin de passer une instance d'une structure et il est clair pour l'appelant quelle option vous obtenez.

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

Ensuite, la création d'instances ressemble à ceci:

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

Ou, pour que cela ressemble davantage à l'approche de classe de balises, utilisez une énumération à valeur unique au lieu de la classe de balises:

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

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

Il n'y a alors que deux façons de créer une instance:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};
TimK
la source
Je suis d'accord avec vous: les énumérations sont plus simples. Mais peut-être avez-vous oublié cette ligne:event_counts() : counts{} {}
bleuâtre
@bluish, mon intention n'était pas d'initialiser countsinconditionnellement, mais seulement quand INITest défini.
TimK
@bluish Je pense que la principale raison de choisir une classe de balises n'est pas de parvenir à la simplicité, mais de signaler qu'un objet non initialisé est spécial, c'est-à-dire qu'il utilise une fonction d'optimisation plutôt qu'une partie normale de l'interface de classe. Les deux boolet enumsont décents, mais nous devons être conscients que l'utilisation de paramètres au lieu de la surcharge a une nuance sémantique quelque peu différente. Dans le premier, vous paramétrez clairement un objet, donc la position initialisée / non initialisée devient son état, tandis que passer un objet tag à ctor revient plus à demander à la classe d'effectuer une conversion. Ce n'est donc pas une question de choix syntaxique de l'OMI.
doc
@TimK Mais l'OP veut que le comportement par défaut soit l'initialisation du tableau, donc je pense que votre solution à la question devrait inclure event_counts() : counts{} {}.
bleuâtre
@bluish Dans ma suggestion d'origine countsest initialisée par std::fillsauf si NO_INITest demandé. L'ajout du constructeur par défaut comme vous le suggérez ferait deux façons différentes de faire l'initialisation par défaut, ce qui n'est pas une bonne idée. J'ai ajouté une autre approche qui évite d'utiliser std::fill.
TimK
1

Vous voudrez peut-être envisager une initialisation en deux phases pour votre classe:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

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

Le constructeur ci-dessus n'initialise pas le tableau à zéro. Pour mettre les éléments du tableau à zéro, vous devez appeler la fonction membre set_zero()après la construction.

眠 り ネ ロ ク
la source
7
Merci, j'ai envisagé cette approche mais je veux quelque chose qui garde le défaut par défaut - c'est-à-dire zéro par défaut, et seulement dans quelques endroits sélectionnés, je remplace le comportement par celui qui n'est pas sûr.
BeeOnRope
3
Cela nécessitera des précautions supplémentaires, mais les utilisations supposées non initialisées. C'est donc une source supplémentaire de bugs par rapport à la solution OPs.
noyer
@BeeOnRope pourrait également fournir std::functionun argument constructeur avec quelque chose de similaire à l' set_zeroargument par défaut. Vous passeriez alors une fonction lambda si vous voulez un tableau non initialisé.
doc
1

Je le ferais comme ça:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

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

Le compilateur sera suffisamment intelligent pour ignorer tout le code lorsque vous utilisez event_counts(false), et vous pouvez dire exactement ce que vous voulez dire au lieu de rendre l'interface de votre classe si bizarre.

Matt Timmermans
la source
8
Vous avez raison sur l'efficacité, mais les paramètres booléens ne permettent pas d'obtenir un code client lisible. Lorsque vous lisez et voyez la déclaration event_counts(false), qu'est-ce que cela signifie? Vous n'avez aucune idée sans revenir en arrière et regarder le nom du paramètre. Mieux vaut au moins utiliser une énumération ou, dans ce cas, une classe sentinelle / tag comme indiqué dans la question. Ensuite, vous obtenez une déclaration plus semblable à event_counts(no_init), ce qui est évident pour tout le monde dans son sens.
Cody Gray
Je pense que c'est aussi une solution décente. Vous pouvez ignorer le ctor par défaut et utiliser la valeur par défaut event_counts(bool initCountr = true).
doc
De plus, ctor doit être explicite.
doc
malheureusement, actuellement, C ++ ne prend pas en charge les paramètres nommés, mais nous pouvons utiliser boost::parameteret appeler event_counts(initCounts = false)pour plus de lisibilité
phuclv
1
Curieusement, @doc estevent_counts(bool initCounts = true) en fait un constructeur par défaut, car chaque paramètre a une valeur par défaut. L'exigence est juste qu'il puisse être appelé sans spécifier d'arguments, peu importe s'il est sans event_counts ec;paramètre ou utilise des valeurs par défaut.
Justin Time - Rétablir Monica
1

J'utiliserais une sous-classe juste pour économiser un peu de frappe:

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

Vous pouvez vous débarrasser de la classe factice en changeant l'argument du constructeur non initialisé en boolou intou quelque chose, car il n'a plus besoin d'être mnémonique.

Vous pouvez également échanger l'héritage et définir events_count_no_initavec un constructeur par défaut comme Evg suggéré dans leur réponse, puis avoir events_countla sous-classe:

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{} {}
};
Ross Ridge
la source
C'est une idée intéressante, mais j'ai également l'impression que l'introduction d'un nouveau type entraînera des frictions. Par exemple, quand je veux en fait un non initialisé event_counts, je veux qu'il soit de type event_count, non event_count_uninitialized, donc je devrais couper à la construction comme event_counts c = event_counts_no_init{};, ce qui, je pense, élimine la plupart des économies de frappe.
BeeOnRope
@BeeOnRope Eh bien, dans la plupart des cas, un event_count_uninitializedobjet est un event_countobjet. C'est tout l'héritage, ce ne sont pas des types complètement différents.
Ross Ridge
D'accord, mais le hic est avec "pour la plupart des fins". Ils ne sont pas interchangeables - par exemple, si vous essayez de voir assigner ecuà eccela fonctionne, mais pas l'inverse. Ou si vous utilisez des fonctions de modèle, elles sont de types différents et se terminent par des instanciations différentes même si le comportement finit par être identique (et parfois ce ne sera pas le cas, par exemple, avec des membres de modèle statiques). Surtout avec une utilisation intensive de autocela peut certainement surgir et être source de confusion: je ne voudrais pas que la façon dont un objet a été initialisé soit reflétée en permanence dans son type.
BeeOnRope