Idiomatyczny sposób rozróżnienia dwóch konstruktorów zero-arg

41

Mam taką klasę:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Zwykle chcę domyślnie (zero) zainicjować countstablicę, jak pokazano.

Jednak w wybranych lokalizacjach zidentyfikowanych przez profilowanie chciałbym powstrzymać inicjalizację tablicy, ponieważ wiem, że tablica wkrótce zostanie zastąpiona, ale kompilator nie jest wystarczająco inteligentny, aby to rozgryźć.

Jaki jest idiomatyczny i skuteczny sposób stworzenia takiego „wtórnego” konstruktora zero-arg?

Obecnie używam klasy znaczników, uninit_tagktóra jest przekazywana jako fikcyjny argument, na przykład:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Następnie wywołuję konstruktor bez inicjacji, tak jak event_counts c(uninit_tag{});gdy chcę wyłączyć konstrukcję.

Jestem otwarty na rozwiązania, które nie wymagają stworzenia sztucznej klasy lub są w jakiś sposób bardziej wydajne itp.

BeeOnRope
źródło
„ponieważ wiem, że tablica wkrótce zostanie zastąpiona” Czy jesteś w 100% pewien, że Twój kompilator nie wykonuje już takiej optymalizacji? sprawa w punkcie: gcc.godbolt.org/z/bJnAuJ
Frank
6
@Frank - Wydaje mi się, że odpowiedź na twoje pytanie znajduje się w drugiej połowie cytowanego zdania? Nie należy do pytania, ale mogą się zdarzyć różne rzeczy: (a) często kompilator po prostu nie jest wystarczająco silny, aby wyeliminować martwe magazyny (b) czasami tylko podzbiór elementów jest nadpisywany, co pokonuje optymalizacja (ale tylko ten sam podzbiór jest później odczytywany) (c) czasami kompilator może to zrobić, ale zostaje pokonany, np. ponieważ metoda nie jest wbudowana.
BeeOnRope
Czy masz innych konstruktorów w swojej klasie?
NathanOliver
1
@Frank - eh, twój przypadek pokazuje, że gcc nie eliminuje martwych sklepów? Właściwie, jeśli zgadłeś, że pomyślałem, że gcc rozwiąże ten bardzo prosty przypadek, ale jeśli zawiedzie tutaj, wyobraź sobie nieco bardziej skomplikowany przypadek!
BeeOnRope
1
@uneven_mark - tak, gcc 9.2 robi to na -O3 (ale ta optymalizacja jest rzadka w porównaniu z -O2, IME), ale wcześniejsze wersje tego nie zrobiły. Ogólnie rzecz biorąc, eliminacja martwych magazynów jest rzeczą, ale jest bardzo delikatna i podlega wszystkim zwykłym zastrzeżeniom, takim jak kompilator, który widzi martwe magazyny w tym samym czasie, gdy widzi dominujące sklepy. Mój komentarz miał na celu wyjaśnienie, co Frank chciał powiedzieć, ponieważ powiedział „przypadek: (godbolt link)”, ale link pokazuje, że oba sklepy są wykonywane (więc może coś mi brakuje).
BeeOnRope

Odpowiedzi:

33

Rozwiązanie, które już masz, jest poprawne i jest dokładnie tym, co chciałbym zobaczyć, gdybym sprawdzał twój kod. Jest tak wydajny, jak to możliwe, jasny i zwięzły.

John Zwinck
źródło
1
Głównym problemem jest to, czy powinienem zadeklarować nowy uninit_tagsmak w każdym miejscu, w którym chcę używać tego idiomu. Miałem nadzieję, że istnieje już coś w rodzaju takiego wskaźnika, być może w std::.
BeeOnRope
9
Nie ma oczywistego wyboru ze standardowej biblioteki. Nie zdefiniowałbym nowego znacznika dla każdej klasy, w której chcę tej funkcji - zdefiniowałbym no_initznacznik obejmujący cały projekt i używałbym go we wszystkich moich klasach, w których byłby potrzebny.
John Zwinck
2
Wydaje mi się, że standardowa biblioteka ma męskie znaczniki do różnicowania iteratorów i tego typu rzeczy oraz dwóch std::piecewise_construct_ti std::in_place_t. Żaden z nich nie wydaje się rozsądny w użyciu tutaj. Być może chciałbyś zdefiniować globalny obiekt tego typu, który będzie zawsze używany, więc nie potrzebujesz nawiasów klamrowych w każdym wywołaniu konstruktora. STL robi to z std::piecewise_constructza std::piecewise_construct_t.
n314159
To nie jest tak wydajne, jak to możliwe. Na przykład w konwencji wywoływania AArch64 znacznik musi być przypisany do stosu, z efektami domina (nie można go też wywołać ...): godbolt.org/z/6mSsmq
TLW
1
@TLW Po dodaniu ciała do konstruktorów nie ma alokacji stosu, godbolt.org/z/vkCD65
R2RT
8

Jeśli treść konstruktora jest pusta, można ją pominąć lub ustawić domyślnie:

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

Wtedy domyślna inicjalizacja event_counts counts; pozostawi counts.countsniezainicjowaną (tutaj domyślna inicjalizacja nie jest możliwa ), a inicjalizacja event_counts counts{}; wartości spowoduje zainicjowanie wartości counts.counts, skutecznie wypełniając ją zerami.

Evg
źródło
3
Ale znowu musisz pamiętać, aby użyć inicjalizacji wartości, a OP chce, aby była ona domyślnie bezpieczna.
dok.
@doc, zgadzam się. To nie jest dokładne rozwiązanie tego, czego chce OP. Ale ta inicjalizacja naśladuje wbudowane typy. Ponieważ int i;akceptujemy, że nie jest on inicjowany na zero. Być może powinniśmy również zaakceptować, że event_counts counts;nie jest to inicjalizacja zerowa i ustawić event_counts counts{};nasz nowy domyślny.
Evg
6

Podoba mi się twoje rozwiązanie. Być może rozważałeś także zagnieżdżoną strukturę i zmienną statyczną. Na przykład:

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

};

W przypadku zmiennej statycznej wywołanie niezainicjowanego konstruktora może wydawać się wygodniejsze:

event_counts e(event_counts::uninit);

Możesz oczywiście wprowadzić makro, aby zapisać pisanie i uczynić go bardziej systematyczną funkcją

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

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}
dok
źródło
3

Myślę, że wyliczanie jest lepszym wyborem niż klasa tagów lub bool. Nie musisz przekazywać wystąpienia struktury i od osoby dzwoniącej jasno wynika, którą opcję otrzymujesz.

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

Następnie tworzenie instancji wygląda następująco:

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

Lub, aby uczynić to bardziej zbliżonym do klasy tagów, użyj wyliczenia pojedynczej wartości zamiast klasy tag:

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

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

Istnieją tylko dwa sposoby utworzenia instancji:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};
TimK
źródło
Zgadzam się z tobą: enum jest prostsze. Ale może zapomniałeś o tej linii:event_counts() : counts{} {}
niebieskawy
@bluish, moim zamiarem nie była inicjalizacja countsbezwarunkowa, ale tylko wtedy, gdy INITjest ustawiona.
TimK
@bluish Myślę, że głównym powodem wyboru klasy znaczników nie jest osiągnięcie prostoty, ale zasygnalizowanie, że niezainicjowany obiekt jest wyjątkowy, tzn. wykorzystuje funkcję optymalizacji, a nie normalną część interfejsu klasy. Zarówno booli enumsą przyzwoite, ale musimy mieć świadomość, że za pomocą parametru zamiast przeciążenia ma nieco inny odcień semantyczną. W poprzednim wyraźnie sparametryzujesz obiekt, stąd postawa zainicjalizowana / niezainicjowana staje się jego stanem, podczas gdy przekazanie obiektu znacznika do ctor jest bardziej jak poproszenie klasy o wykonanie konwersji. Więc nie jest to IMO kwestią wyboru składniowego.
dok.
@TimK Ale OP chce, aby domyślnym zachowaniem była inicjalizacja tablicy, więc myślę, że twoje rozwiązanie pytania powinno obejmować event_counts() : counts{} {}.
niebieskawy
@bluish W mojej oryginalnej sugestii countsinicjowana jest przez, std::fillchyba że NO_INITzostanie o to poproszona. Dodanie domyślnego konstruktora, tak jak sugerujesz, stworzyłoby dwa różne sposoby domyślnej inicjalizacji, co nie jest dobrym pomysłem. Dodałem inne podejście, które pozwala uniknąć używania std::fill.
TimK
1

Możesz rozważyć inicjację dwufazową dla swojej klasy:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

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

Konstruktor powyżej nie inicjuje tablicy do zera. Aby ustawić elementy tablicy na zero, musisz wywołać funkcję elementu set_zero()po zakończeniu budowy.

眠 り ネ ロ ク
źródło
7
Dzięki, zastanowiłem się nad tym podejściem, ale chcę czegoś, co zapewni domyślne bezpieczeństwo - tzn. Domyślnie zero i tylko w kilku wybranych miejscach zastąpię to zachowanie niebezpiecznym.
BeeOnRope
3
Będzie to wymagało szczególnej staranności, z wyjątkiem zastosowań, które powinny być niezainicjowane. Jest to więc dodatkowe źródło błędów w stosunku do rozwiązania OP.
orzech
@BeeOnRope można również podać std::functionjako argument konstruktora z czymś podobnym do set_zeroargumentu domyślnego. Następnie przekazałbyś funkcję lambda, jeśli chcesz niezainicjowanej tablicy.
dok.
1

Zrobiłbym to w ten sposób:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

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

Kompilator będzie wystarczająco inteligentny, aby pominąć cały kod, gdy go używasz event_counts(false), i możesz powiedzieć dokładnie, co masz na myśli, zamiast sprawić, że interfejs twojej klasy będzie tak dziwny.

Matt Timmermans
źródło
8
Masz rację co do wydajności, ale parametry boolowskie nie zapewniają czytelnego kodu klienta. Kiedy czytasz dalej i widzisz deklarację event_counts(false), co to oznacza? Nie masz pojęcia, nie wracając i nie patrząc na nazwę parametru. Lepiej przynajmniej użyć wyliczenia lub, w tym przypadku, klasy wartownika / tagu, jak pokazano w pytaniu. Następnie otrzymujesz deklarację bardziej podobną event_counts(no_init), co dla każdego jest oczywiste.
Cody Gray
Myślę, że to także przyzwoite rozwiązanie. Możesz odrzucić domyślny ctor i użyć wartości domyślnej event_counts(bool initCountr = true).
dok.
Ponadto ctor powinien być wyraźny.
doc
niestety obecnie C ++ nie obsługuje nazwanych parametrów, ale możemy użyć boost::parameteri wezwać event_counts(initCounts = false)do odczytu
phuclv
1
Zabawne, @doc, event_counts(bool initCounts = true)faktycznie jest konstruktor domyślny, z powodu każdego parametru mającego wartość domyślną. Wymagane jest tylko, aby można było wywoływać je bez podawania argumentów, event_counts ec;nie obchodzi go, czy jest ono bez parametrów, czy używa wartości domyślnych.
Justin Time - Przywróć Monikę
1

Użyłbym podklasy, żeby zaoszczędzić trochę pisania:

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

Można pozbyć klasy manekina przez zmianę argument konstruktora nie do inicjalizacji boollub intczy coś, bo nie ma już być pamięciowy.

Możesz także zamienić dziedzictwo i zdefiniować events_count_no_initza pomocą domyślnego konstruktora, takiego jak sugerowany w odpowiedzi Evg, a następnie events_countbyć podklasą:

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
źródło
To ciekawy pomysł, ale mam też wrażenie, że wprowadzenie nowego typu spowoduje tarcie. Np. Kiedy naprawdę chcę zainicjować niezainicjowaną event_counts, chcę, żeby była tego rodzaju event_count, event_count_uninitializedwięc nie powinienem, więc powinienem kroić na event_counts c = event_counts_no_init{};etapie budowy , co, jak sądzę, eliminuje większość oszczędności podczas pisania.
BeeOnRope
@BeeOnRope Cóż, dla większości celów event_count_uninitializedobiekt jest event_countobiektem. To jest sedno dziedziczenia, nie są to zupełnie różne typy.
Ross Ridge
Zgadzam się, ale rub to „dla większości celów”. Nie są one wymienne - np. Jeśli spróbujesz zobaczyć, że przypisanie ecudo ecniego działa, ale nie na odwrót. Lub jeśli używasz funkcji szablonów, są to różne typy i kończą się różnymi instancjami, nawet jeśli zachowanie kończy się identyczne (a czasem nie będzie tak np. Ze statycznymi elementami szablonu). Zwłaszcza przy intensywnym użyciu autotego może z pewnością pojawić się i być mylące: nie chciałbym, aby sposób inicjalizacji obiektu został trwale odzwierciedlony w jego typie.
BeeOnRope