Zainicjuj wiele stałych elementów klasy za pomocą jednego wywołania funkcji C ++

50

Jeśli mam dwie różne stałe zmienne składowe, które należy zainicjować na podstawie tego samego wywołania funkcji, czy istnieje sposób, aby to zrobić bez dwukrotnego wywołania funkcji?

Na przykład klasa ułamkowa, w której licznik i mianownik są stałe.

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

Powoduje to stratę czasu, ponieważ funkcja GCD jest wywoływana dwukrotnie. Możesz także zdefiniować nowego członka klasy gcd_a_bi najpierw przypisać wyjście gcd do tego na liście inicjalizatora, ale wtedy doprowadziłoby to do marnowania pamięci.

Czy istnieje sposób, aby to zrobić bez marnowania wywołań funkcji lub pamięci? Czy możesz stworzyć tymczasowe zmienne na liście inicjalizatora? Dziękuję Ci.

Pytanie 0
źródło
5
Czy masz dowód, że „funkcja GCD jest wywoływana dwukrotnie”? Jest wspomniany dwa razy, ale to nie to samo, co kompilator emitujący kod, który wywołuje go dwukrotnie. Kompilator może wywnioskować, że jest to czysta funkcja i ponownie użyć swojej wartości przy drugim wzmiance.
Eric Towers
6
@EricTowers: Tak, w niektórych przypadkach kompilatory mogą czasem obejść problem w praktyce. Ale tylko wtedy, gdy widzą definicję (lub adnotację w obiekcie), w przeciwnym razie nie ma możliwości udowodnienia, że ​​jest czysta. Państwo powinno skompilować z optymalizacji włączona link-time, ale nie każdy ma. Funkcja może znajdować się w bibliotece. Lub rozważ przypadek funkcji, która ma skutki uboczne, a wywołanie jej dokładnie raz jest kwestią poprawności?
Peter Cordes,
@EricTowers Ciekawy punkt. Właściwie to próbowałem to sprawdzić, umieszczając instrukcję print w funkcji GCD, ale teraz zdaję sobie sprawę, że to uniemożliwiłoby jej czystą funkcję.
Qq0
@ Qq0: Możesz to sprawdzić patrząc na kompilator wygenerowany asm, np. Używając eksploratora kompilatora Godbolt z gcc lub clang -O3. Ale prawdopodobnie dla każdej prostej implementacji testowej w rzeczywistości wstawiłoby wywołanie funkcji. Jeśli użyjesz __attribute__((const))lub użyjesz prototypu bez podania widocznej definicji, powinien on pozwolić GCC lub clang na eliminację typowej podwyrażenia (CSE) między dwoma wywołaniami z tym samym argumentem. Zauważ, że odpowiedź Drew działa nawet w przypadku nieoczyszczonych funkcji, więc jest o wiele lepsza i powinieneś jej używać, gdy func może nie być wbudowany.
Peter Cordes,
Zasadniczo najlepiej unikać niestatycznych zmiennych stałych składowych. Jeden z niewielu obszarów, w których const wszystko często nie ma zastosowania. Na przykład nie możesz przypisać obiektów klasy. Możesz umieścić backback w wektorze, ale tylko o ile limit pojemności nie spowoduje zmiany rozmiaru.
doug

Odpowiedzi:

67

Czy istnieje sposób, aby to zrobić bez marnowania wywołań funkcji lub pamięci?

Tak. Można to zrobić za pomocą konstruktora delegującego , wprowadzonego w C ++ 11.

Konstruktor delegujący to bardzo skuteczny sposób na uzyskanie wartości tymczasowych potrzebnych do konstrukcji przed zainicjowaniem jakichkolwiek zmiennych składowych.

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
źródło
Czy z uwagi na zainteresowanie narzut wywołany innym konstruktorem byłby znaczący?
Qq0
1
@ Qq0 Można tutaj zaobserwować, że nie ma narzutu z włączonymi skromnymi optymalizacjami.
Drew Dormann,
2
@ Qq0: C ++ został zaprojektowany wokół nowoczesnych kompilatorów optymalizujących. Mogą w trywialny sposób wprowadzić tę delegację, zwłaszcza jeśli uczynisz ją widoczną w definicji klasy (w .h), nawet jeśli rzeczywista definicja konstruktora nie jest widoczna dla wstawiania. tzn. gcd()wywołanie będzie wstawiane do każdego miejsca wywoławczego konstruktora i pozostawi tylko callprywatny konstruktor z 3 operandami.
Peter Cordes,
10

Zmienne składowe są inicjowane według kolejności, w której zostały zadeklarowane podczas deklowania klas, dlatego można wykonać następujące czynności (matematyczne)

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

Nie ma potrzeby wywoływania innych konstruktorów ani nawet ich tworzenia.

asmmo
źródło
6
ok, to działa szczególnie na GCD, ale wiele innych przypadków użycia prawdopodobnie nie może wyprowadzić drugiej stałej z argumentów i pierwszej. I jak napisano, ma jeden dodatkowy podział, który jest kolejnym minusem w porównaniu z ideałem, którego kompilator może nie zoptymalizować. GCD może kosztować tylko jedną dywizję, więc może to być prawie tak samo złe, jak dwukrotne wywołanie GCD. (Zakładając, że podział ten dominuje koszt innych operacji, jak to często ma miejsce w nowoczesnych procesorach).
Peter Cordes
@PeterCordes, ale drugie rozwiązanie ma dodatkowe wywołanie funkcji i przydziela więcej pamięci instrukcji.
asmmo
1
Mówisz o konstruktorze delegującym Drew? Może to oczywiście skłaniać Fraction(a,b,gcd(a,b))delegację do osoby dzwoniącej, prowadząc do niższych całkowitych kosztów. To wbudowanie jest łatwiejsze dla kompilatora niż cofnięcie dodatkowego podziału w tym. Nie próbowałem tego na godbolt.org, ale możesz, jeśli jesteś ciekawy. Użyj gcc lub clang, -O3jak zwykła kompilacja. (C ++ został zaprojektowany w oparciu o założenie nowoczesnego kompilatora optymalizującego, stąd funkcje takie jak constexpr)
Peter Cordes,
-3

@Drew Dormann podał rozwiązanie podobne do tego, co miałem na myśli. Ponieważ OP nigdy nie wspomina o niemożności modyfikacji ctor, można to wywołać za pomocą Fraction f {a, b, gcd(a, b)}:

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

Tylko w ten sposób nie ma drugiego wywołania funkcji, konstruktora lub w inny sposób, więc nie jest to stracony czas. I nie jest to zmarnowana pamięć, ponieważ tymczasowe i tak musiałoby zostać utworzone, więc równie dobrze możesz z niej skorzystać. Pozwala to również uniknąć dodatkowego podziału.

zainteresowany obywatel
źródło
3
Twoja edycja sprawia, że ​​nawet nie odpowiada na pytanie. Teraz wymagasz od dzwoniącego przekazania trzeciego argumentu? Twoja oryginalna wersja wykorzystująca przypisanie wewnątrz ciała konstruktora nie działa const, ale działa przynajmniej dla innych typów. A jakiego dodatkowego podziału „unikasz”? Masz na myśli odpowiedź kontra asmmo?
Peter Cordes,
1
Ok, usunąłem moją opinię, gdy wyjaśniłeś już swój punkt widzenia. Ale wydaje się to dość okropne i wymaga ręcznego wprowadzenia części pracy konstruktora do każdego programu wywołującego. Jest to przeciwieństwo SUCHEGO (nie powtarzaj się) i podsumowania odpowiedzialności / wewnętrznych elementów klasy. Większość ludzi nie uznałaby tego za akceptowalne rozwiązanie. Biorąc pod uwagę, że istnieje sposób C ++ 11, aby to zrobić czysto, nikt nigdy nie powinien tego robić, chyba że utknie w starszej wersji C ++, a klasa ma bardzo mało wywołań do tego konstruktora.
Peter Cordes,
2
@aconcernedcitizen: Nie mam na myśli wydajności, mam na myśli jakość kodu. Na swój sposób, jeśli kiedykolwiek zmieniłeś sposób, w jaki ta klasa działała wewnętrznie, musisz znaleźć wszystkie wywołania konstruktora i zmienić trzeci argument. Ten dodatkowy ,gcd(foo, bar)kod jest dodatkowym kodem, który mógłby i dlatego powinien zostać uwzględniony w każdej witrynie wywoławczej w źródle . Jest to problem dotyczący konserwacji / czytelności, a nie wydajności. Kompilator najprawdopodobniej wstawi go w czasie kompilacji, który chcesz zwiększyć wydajność.
Peter Cordes,
1
@PeterCordes Masz rację, teraz widzę, że mój umysł był skupiony na rozwiązaniu i zignorowałem wszystko inne. Tak czy inaczej odpowiedź pozostaje, choćby dla zawstydzenia. Ilekroć będę miał co do tego wątpliwości, będę wiedział, gdzie szukać.
zainteresowany obywatel
1
Weź również pod uwagę przypadek Fraction f( x+y, a+b ); Aby napisać to po swojemu, będziesz musiał napisać BadFraction f( x+y, a+b, gcd(x+y, a+b) );lub użyć tmp vars. Albo jeszcze gorzej, co jeśli chcesz napisać Fraction f( foo(x), bar(y) );- wtedy potrzebujesz strony wywołującej, aby zadeklarować niektóre zmienne tmp do przechowywania wartości zwrotnych lub wywołać te funkcje ponownie i mieć nadzieję, że kompilator CSE je odejdzie, czego unikamy. Czy chcesz debugować przypadek mieszania argumentów przez jednego z rozmówców, gcdaby nie był to tak naprawdę GCD pierwszych 2 argumentów przekazanych do konstruktora? Nie? Więc nie pozwól, aby ten błąd był możliwy.
Peter Cordes,