Kiedy NIE należy używać wirtualnych niszczycieli?


48

Wierzyłem, że wielokrotnie szukałem wirtualnych destruktorów, większość wspomina o celu wirtualnych destruktorów i dlaczego potrzebujesz wirtualnych destruktorów. Myślę też, że w większości przypadków destruktory muszą być wirtualne.

Zatem pytanie brzmi: dlaczego c ++ domyślnie nie ustawia wirtualnych wszystkich destruktorów? lub w innych pytaniach:

Kiedy NIE muszę używać wirtualnych niszczycieli?

W takim przypadku NIE powinienem używać wirtualnych niszczycieli?

Jaki jest koszt korzystania z wirtualnych niszczycieli, jeśli go używam, nawet jeśli nie jest potrzebny?


6
A jeśli twoja klasa nie powinna być dziedziczona? Spójrz na wiele standardowych klas bibliotek, niewiele ma funkcje wirtualne, ponieważ nie są zaprojektowane do dziedziczenia.
Jakiś programista koleś

4
Myślę też, że w większości przypadków destruktory muszą być wirtualne. Nie. Ani trochę. Tak uważają tylko ci, którzy nadużywają dziedziczenia (zamiast faworyzowania kompozycji). Widziałem całe aplikacje z tylko kilkoma klasami podstawowymi i funkcjami wirtualnymi.
Matthieu M.

1
@underscore_d Przy typowych implementacjach, dla każdej klasy polimorficznej generowany byłby dodatkowy kod, chyba że wszystkie takie niejawne rzeczy zostałyby zdirirtualizowane i zoptymalizowane. W ramach wspólnych ABI obejmuje to co najmniej jeden vtable dla każdej klasy. Układ klasy również musi zostać zmieniony. Po opublikowaniu takiej klasy jako części jakiegoś publicznego interfejsu nie można wrócić w sposób niezawodny, ponieważ zmiana go ponownie złamałaby kompatybilność ABI, ponieważ oczywiście źle (jeśli to możliwe) jest oczekiwanie na dewiralizację jako umowy interfejsu w ogóle.
FrankHB

1
@underscore_d Wyrażenie „w czasie kompilacji” jest niedokładne, ale myślę, że oznacza to, że wirtualny destruktor nie może być trywialny ani nie ma określonego constexpr, więc trudno jest uniknąć generowania dodatkowego kodu (chyba że całkowicie unikniesz zniszczenia takich obiektów), więc w mniejszym lub większym stopniu zaszkodziłoby to środowisku wykonawczemu.
FrankHB

2
@underscore_d „Wskaźnik” wydaje się czerwony śledź. Prawdopodobnie powinien to być wskaźnik do elementu członkowskiego (który z definicji nie jest wskaźnikiem). W przypadku zwykłych ABI wskaźnik do elementu często nie mieści się w słowie maszynowym (jako typowe wskaźniki), a zmiana klasy z niepolimorficznej na polimorficzną często zmieniałaby rozmiar wskaźnika na element tej klasy.
FrankHB

Odpowiedzi:


41

Jeśli dodasz wirtualny destruktor do klasy:

  • w większości (wszystkich?) bieżących implementacji C ++ każda instancja obiektu tej klasy musi przechowywać wskaźnik do wirtualnej tabeli wysyłania dla typu środowiska wykonawczego, a sama wirtualna tabela wysyłania jest dodawana do obrazu wykonywalnego

  • adres wirtualnej tabeli wysyłania niekoniecznie jest prawidłowy dla różnych procesów, co może uniemożliwić bezpieczne współdzielenie takich obiektów w pamięci współdzielonej

  • osadzony wirtualny wskaźnik frustruje tworzenie klasy z układem pamięci pasującym do znanego formatu wejściowego lub wyjściowego (na przykład, więc Price_Tick*można skierować bezpośrednio na odpowiednio wyrównaną pamięć w przychodzącym pakiecie UDP i użyć do parsowania / dostępu lub zmiany danych, lub umieszczanie newtakiej klasy w celu zapisywania danych w pakiecie wychodzącym)

  • same wywołania destruktora mogą - pod pewnymi warunkami - być wysyłane wirtualnie, a zatem poza linią, podczas gdy nie-wirtualne niszczyciele mogą być nachylone lub zoptymalizowane, jeśli są trywialne lub nieistotne dla dzwoniącego

Argument „nieprzeznaczony do dziedziczenia” nie byłby praktycznym powodem, dla którego nie zawsze miałby się wirtualny destruktor, gdyby nie było gorzej w praktyczny sposób, jak wyjaśniono powyżej; ale biorąc pod uwagę, że jest gorzej, jest to główne kryterium określające, kiedy należy ponieść koszty: domyślnie mieć wirtualny destruktor, jeśli twoja klasa ma być używana jako klasa podstawowa . Nie zawsze jest to konieczne, ale zapewnia, że ​​klasy w hierarchii mogą być używane bardziej swobodnie, bez przypadkowego niezdefiniowanego zachowania, jeśli wywoływany destruktor klasy zostanie wywołany za pomocą wskaźnika klasy podstawowej lub odwołania.

„w większości przypadków destruktory muszą być wirtualne”

Nie tak ... wiele klas nie ma takiej potrzeby. Jest tak wiele przykładów, w których niepotrzebne jest ich wyliczenie, ale po prostu przejrzyj swoją Standardową Bibliotekę lub powiedz „boost”, a zobaczysz, że istnieje znaczna większość klas, które nie mają wirtualnych destruktorów. W doładowaniu 1,53 liczę 72 wirtualnych destrukcyjnych z 494.


23

W takim przypadku NIE powinienem używać wirtualnych niszczycieli?

  1. Dla konkretnej klasy, która nie chce być dziedziczona.
  2. Dla klasy bazowej bez usuwania polimorficznego. Żaden klient nie powinien mieć możliwości usuwania polimorficznego za pomocą wskaźnika do bazy.

BTW,

W którym przypadku należy użyć wirtualnych niszczycieli?

Dla klas podstawowych z usuwaniem polimorficznym.


7
+1 za # 2, szczególnie bez usuwania polimorficznego . Jeśli twojego destruktora nigdy nie można wywołać za pomocą wskaźnika bazowego, uczynienie go wirtualnym jest niepotrzebne i zbędne, szczególnie jeśli twoja klasa nie była wcześniej wirtualna (więc staje się nowo rozdęta przez RTTI). Aby uchronić się przed naruszeniem tego przez dowolnego użytkownika, zgodnie z zaleceniem Herb Suttera, dtor klasy podstawowej będzie chroniony i nie będzie wirtualny, aby można go było wywoływać tylko przez / po pochodnym destruktorze.
underscore_d

@underscore_d imho, że ważnym punktem, który przeoczyłem w odpowiedziach, ponieważ w przypadku dziedziczenia jedynym przypadkiem, w którym nie potrzebuję wirtualnego konstruktora, jest to, że mogę się upewnić, że nigdy nie będzie potrzebny
dawniej

14

Jaki jest koszt korzystania z wirtualnych niszczycieli, jeśli go używam, nawet jeśli nie jest potrzebny?

Koszt wprowadzenia dowolnej funkcji wirtualnej do klasy (odziedziczonej lub części definicji klasy) jest prawdopodobnie bardzo stromy (lub nie zależy od obiektu) początkowy koszt wirtualnego wskaźnika przechowywanego na obiekt, tak jak:

struct Integer
{
    virtual ~Integer() {}
    int value;
};

W takim przypadku koszt pamięci jest stosunkowo ogromny. Rzeczywisty rozmiar pamięci instancji klasy będzie teraz często wyglądał następująco na architekturach 64-bitowych:

struct Integer
{
    // 8 byte vptr overhead
    int value; // 4 bytes
    // typically 4 more bytes of padding for alignment of vptr
};

Łącznie dla tej Integerklasy jest 16 bajtów, w przeciwieństwie do zaledwie 4 bajtów. Jeśli przechowujemy milion z nich w macierzy, otrzymamy 16 megabajtów pamięci: dwa razy większy niż typowy 8 MB pamięci podręcznej procesora L3, a powtarzanie tej macierzy wielokrotnie może być wielokrotnie wolniejsze niż ekwiwalent 4 megabajtów bez wirtualnego wskaźnika w wyniku dodatkowych braków pamięci podręcznej i błędów strony.

Koszt wirtualnego wskaźnika na obiekt nie wzrasta jednak wraz z większą liczbą funkcji wirtualnych. Możesz mieć 100 wirtualnych funkcji członka w klasie, a narzut na instancję nadal byłby pojedynczym wirtualnym wskaźnikiem.

Wirtualny wskaźnik jest zwykle bardziej bezpośrednim problemem z ogólnego punktu widzenia. Jednak oprócz wirtualnego wskaźnika na instancję jest koszt na klasę. Każda klasa z funkcjami wirtualnymi generuje vtablew pamięci, która przechowuje adresy funkcji, które powinna faktycznie wywoływać (dyspozycja wirtualna / dynamiczna), gdy wykonywane jest wywołanie funkcji wirtualnej. vptrPrzechowywane na przykład wtedy punkty do tej klasy specyficznych vtable. Narzut ten zwykle stanowi mniejszy problem, ale może nadmuchać rozmiar pliku binarnego i dodać trochę kosztów w czasie wykonywania, jeśli koszty te zostały niepotrzebnie zapłacone za tysiąc klas w złożonej bazie kodu, np. Ta vtablestrona kosztu faktycznie rośnie proporcjonalnie z większą liczbą i więcej funkcji wirtualnych w miksie.

Programiści Java pracujący w obszarach krytycznych pod względem wydajności bardzo dobrze rozumieją tego rodzaju koszty ogólne (choć często opisywane w kontekście boksu), ponieważ typ zdefiniowany przez użytkownika Java domyślnie dziedziczy z centralnej objectklasy bazowej, a wszystkie funkcje w Javie są domyślnie wirtualne (możliwe do zastąpienia ) z natury, chyba że zaznaczono inaczej. W rezultacie Java Integerrównież wymaga 16 bajtów pamięci na platformach 64-bitowych ze względu na vptrmetadane związane ze stylem na instancję i zazwyczaj nie jest możliwe zawinięcie w Javę czegoś takiego jak pojedyncza intklasa bez płacenia za środowisko uruchomieniowe koszt wydajności.

Zatem pytanie brzmi: dlaczego c ++ domyślnie nie ustawia wirtualnych wszystkich destruktorów?

C ++ naprawdę faworyzuje wydajność dzięki podejściu typu „pay as you go”, a także wciąż wielu projektom opartym na sprzęcie metalowym odziedziczonym po C. Nie chce niepotrzebnie uwzględniać kosztów ogólnych związanych z generowaniem vtable i dynamicznym wysyłaniem każda zaangażowana klasa / instancja. Jeśli wydajność nie jest jednym z kluczowych powodów, dla których używasz języka takiego jak C ++, możesz odnieść większe korzyści z innych języków programowania, ponieważ duża część języka C ++ jest mniej bezpieczna i trudniejsza, niż byłoby to możliwe w przypadku często występującej wydajności kluczowy powód, aby faworyzować taki projekt.

Kiedy NIE muszę używać wirtualnych niszczycieli?

Całkiem często. Jeśli klasa nie jest zaprojektowana do dziedziczenia, to nie potrzebuje wirtualnego destruktora i ostatecznie zapłaciłby tylko potencjalnie duży koszt za coś, czego nie potrzebuje. Podobnie, nawet jeśli klasa jest zaprojektowana do dziedziczenia, ale nigdy nie usuwasz instancji podtypu za pomocą wskaźnika bazowego, to również nie wymaga wirtualnego destruktora. W takim przypadku bezpieczną praktyką jest zdefiniowanie chronionego niewirtualnego destruktora, takiego jak:

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

W takim przypadku NIE powinienem używać wirtualnych niszczycieli?

W rzeczywistości łatwiej jest pokryć, kiedy powinieneś używać wirtualnych niszczycieli. Dość często znacznie więcej klas w twojej bazie kodu nie będzie zaprojektowanych do dziedziczenia.

std::vector, na przykład, nie jest przeznaczony do dziedziczenia i zwykle nie powinien być dziedziczony (bardzo chwiejny projekt), ponieważ będzie to podatne na ten problem usuwania wskaźnika podstawowego ( std::vectorumyślnie omija wirtualny destruktor) oprócz niezręcznych problemów z wycinaniem obiektów , jeśli twój klasa pochodna dodaje dowolny nowy stan.

Ogólnie rzecz biorąc, dziedziczona klasa powinna mieć albo publiczny wirtualny niszczyciel, albo chroniony, niewirtualny. Z C++ Coding Standardsrozdziału 50:

50. Upublicznij niszczyciele klasy podstawowej jako publiczne i wirtualne lub chronione i niewirtualne. Aby usunąć lub nie usunąć; to jest pytanie: Jeśli usunięcie wskaźnika przez bazę do bazy powinno być dozwolone, niszczyciel bazy musi być publiczny i wirtualny. W przeciwnym razie powinien być chroniony i niewirusowy.

Jedną z rzeczy, które C ++ zwykle podkreśla w sposób dorozumiany (ponieważ projekty stają się naprawdę kruche i niewygodne, a być może nawet niebezpieczne inaczej) jest idea, że ​​dziedziczenie nie jest mechanizmem zaprojektowanym do późniejszej refleksji. Jest to mechanizm rozszerzalności z uwzględnieniem polimorfizmu, ale taki, który wymaga przewidywania, gdzie jest potrzebna rozszerzalność. W rezultacie twoje klasy podstawowe powinny być zaprojektowane jako pierwiastki hierarchii dziedziczenia z góry, a nie coś, co odziedziczysz później, bez uprzedzenia.

W tych przypadkach, w których po prostu chcesz odziedziczyć do ponownego użycia istniejącego kodu, często zaleca się skład (Zasada złożonego ponownego użycia).


9

Dlaczego c ++ nie ustawia domyślnie wirtualnych wszystkich destruktorów? Koszt dodatkowej przestrzeni dyskowej i wywołania tabeli metod wirtualnych. C ++ jest używany do programowania systemowego, rt o niskim opóźnieniu, gdzie może to być obciążenie.


Niszczycieli nie należy używać przede wszystkim w trudnych systemach czasu rzeczywistego, ponieważ nie można użyć wielu zasobów, takich jak pamięć dynamiczna, aby zapewnić silne gwarancje terminów
Marco A.

9
@MarcoA. Od kiedy niszczyciele oznaczają dynamiczny przydział pamięci?
chbaker0

@ chbaker0 Użyłem „lubię”. Po prostu nie są używane z mojego doświadczenia.
Marco A.

6
To również nonsens, że pamięci dynamicznej nie można używać w twardych systemach czasu rzeczywistego. Dość trywialne jest udowodnienie, że wstępnie skonfigurowana sterta ze stałymi rozmiarami alokacji i bitmapą alokacji albo przydzieli pamięć, albo zwróci stan braku pamięci w czasie potrzebnym na skanowanie tej mapy bitowej.
MSalters

@msalters, które każą mi myśleć: wyobraź sobie program, w którym koszt każdej operacji był przechowywany w systemie typów. Umożliwianie kontroli gwarancji w czasie rzeczywistym.
Yakk

5

To dobry przykład, kiedy nie używać wirtualnego destruktora: Od Scott Meyers:

Jeśli klasa nie zawiera żadnych funkcji wirtualnych, często oznacza to, że nie ma być używana jako klasa podstawowa. Kiedy klasa nie jest przeznaczona do użycia jako klasa podstawowa, wirtualny destruktor jest zwykle złym pomysłem. Rozważ ten przykład na podstawie dyskusji w ARM:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

Jeśli short int zajmuje 16 bitów, obiekt Point może zmieścić się w rejestrze 32-bitowym. Ponadto obiekt Point można przekazać jako 32-bitową liczbę do funkcji napisanych w innych językach, takich jak C lub FORTRAN. Jeśli wirtualny punktator jest wirtualny, sytuacja się zmienia.

W momencie dodawania elementu wirtualnego do klasy dodawany jest wirtualny wskaźnik wskazujący na wirtualną tabelę dla tej klasy.


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.Wut Czy ktoś jeszcze pamięta dobre stare dni, w których mogliśmy wykorzystywać klasy i dziedzictwo do budowania kolejnych warstw członków i zachowań wielokrotnego użytku, bez konieczności dbania o metody wirtualne? No dalej, Scott. Rozumiem sedno, ale to „często” naprawdę sięga.
underscore_d

3

Wirtualny destruktor dodaje koszty działania. Koszt jest szczególnie duży, jeśli klasa nie ma żadnych innych metod wirtualnych. Wirtualny destruktor jest także potrzebny tylko w jednym konkretnym scenariuszu, w którym obiekt jest usuwany lub w inny sposób niszczony przez wskaźnik do klasy podstawowej. W takim przypadku destruktor klasy podstawowej musi być wirtualny, a destruktor dowolnej klasy pochodnej będzie domyślnie wirtualny. Istnieje kilka scenariuszy, w których polimorficzna klasa bazowa jest używana w taki sposób, że destruktor nie musi być wirtualny:

  • Jeśli instancje klas pochodnych nie są przydzielane na stercie, np. Tylko bezpośrednio na stosie lub wewnątrz innych obiektów. (Z wyjątkiem sytuacji, gdy używasz niezainicjowanej pamięci i nowego operatora umieszczania).
  • Jeśli instancje klas pochodnych są przydzielane na stercie, ale usuwanie następuje tylko za pomocą wskaźników do najbardziej pochodnej klasy, np. Istnieje a std::unique_ptr<Derived>, a polimorfizm zachodzi tylko za pomocą wskaźników i referencji niebędących właścicielami. Innym przykładem jest przydzielanie obiektów za pomocą std::make_shared<Derived>(). Można używać std::shared_ptr<Base>tak długo, jak początkowy wskaźnik to std::shared_ptr<Derived>. Wynika to z faktu, że wspólne wskaźniki mają własną dynamiczną dyspozycję dla destruktorów (separator), która niekoniecznie polega na wirtualnym destruktorze klasy bazowej.

Oczywiście każdą konwencję używania obiektów tylko w wyżej wspomniany sposób można łatwo złamać. Dlatego rada Herb Suttera pozostaje tak aktualna jak zawsze: „Destruktory klasy podstawowej powinny być publiczne i wirtualne lub chronione i nie wirtualne”. W ten sposób, jeśli ktoś spróbuje usunąć wskaźnik do klasy podstawowej za pomocą nie-wirtualnego destruktora (-ów), najprawdopodobniej otrzyma błąd naruszenia zasad dostępu w czasie kompilacji.

Z drugiej strony istnieją klasy, które nie są zaprojektowane jako (publiczne) klasy podstawowe. Moje osobiste zalecenie to zrobić je finalw C ++ 11 lub wyższej. Jeśli jest zaprojektowany jako kwadratowy kołek, istnieje duże prawdopodobieństwo, że nie będzie działał dobrze jako okrągły kołek. Jest to związane z moją preferencją posiadania jawnego kontraktu dziedziczenia między klasą podstawową a klasą pochodną, ​​wzorcem projektowym NVI (interfejs niebędący interfejsem wirtualnym), dla abstrakcyjnych, a nie konkretnych klas bazowych, a także z moją niechęcią do chronionych zmiennych składowych, między innymi , ale wiem, że wszystkie te poglądy są do pewnego stopnia kontrowersyjne.


1

Zadeklarowanie destruktora virtualjest konieczne tylko wtedy, gdy planujesz uczynić classdziedziczonym. Zazwyczaj klasy biblioteki standardowej (takie jak std::string) nie zapewniają wirtualnego destruktora, a zatem nie są przeznaczone do podklasowania.


3
Powodem jest podklasowanie + użycie polimorfizmu. Wirtualny destruktor jest wymagany tylko wtedy, gdy potrzebna jest rozdzielczość dynamiczna, to znaczy odwołanie / wskaźnik / cokolwiek do klasy master może faktycznie odnosić się do wystąpienia podklasy.
Michel Billaud

2
@MichelBillaud faktycznie nadal możesz mieć polimorfizm bez wirtualnych lekarzy. Wirtualny dtor jest wymagany TYLKO do usuwania polimorficznego, tj. Wywoływania deletewskaźnika do klasy bazowej.
chbaker0

1

W konstruktorze pojawi się narzut związany z tworzeniem vtable (jeśli nie masz innych funkcji wirtualnych, w takim przypadku PRAWDOPODOBNIE, ale nie zawsze, powinieneś także mieć wirtualny destruktor). A jeśli nie masz żadnych innych funkcji wirtualnych, powoduje to, że Twój obiekt jest o jeden rozmiar większy niż jest to konieczne. Oczywiście zwiększony rozmiar może mieć duży wpływ na małe obiekty.

Odczytywana jest dodatkowa pamięć, aby uzyskać tabelę vtable, a następnie wywołać funkcję niebezpośrednią przez to, co jest narzutem na nie-wirtualny destruktor, gdy wywoływany jest destruktor. I oczywiście w konsekwencji generowany jest dodatkowy kod dla każdego wywołania destruktora. Dzieje się tak w przypadkach, w których kompilator nie może wydedukować rzeczywistego typu - w przypadkach, w których może wydedukować rzeczywisty typ, kompilator nie użyje vtable, ale wywoła bezpośrednio destruktor.

Państwo powinno mieć wirtualny destruktor jeśli klasa ma służyć jako klasy bazowej, zwłaszcza jeśli może to być tworzone / zniszczone za pośrednictwem innego podmiotu niż kod, który wie, jakiego typu jest to w stworzeniu, to trzeba wirtualnego destruktora.

Jeśli nie jesteś pewien, użyj wirtualnego destruktora. Łatwiej jest usunąć wirtualny, jeśli pojawia się jako problem, niż próbować znaleźć błąd spowodowany przez „brak wywołania odpowiedniego destruktora”.

Krótko mówiąc, nie powinieneś mieć wirtualnego destruktora, jeśli: 1. Nie masz żadnych funkcji wirtualnych. 2. Nie wyprowadzaj się z klasy (zaznacz ją finalw C ++ 11, w ten sposób kompilator powie, czy spróbujesz z niej wywnioskować).

W większości przypadków tworzenie i niszczenie nie stanowi znacznej części czasu spędzonego na użyciu konkretnego obiektu, chyba że istnieje „dużo treści” (utworzenie ciągu 1 MB zajmie oczywiście trochę czasu, ponieważ co najmniej 1 MB danych musi kopiowane z dowolnego miejsca). Zniszczenie ciągu 1 MB nie jest gorsze niż zniszczenie ciągu 150B, oba będą wymagać cofnięcia przydziału pamięci ciągu, i niewiele więcej, więc czas spędzony tam jest zwykle taki sam [chyba że jest to kompilacja debugowania, w której zwolnienie często wypełnia pamięć „wzór trucizny” - ale nie w ten sposób uruchomisz swoją prawdziwą aplikację w produkcji].

Krótko mówiąc, jest niewielki nad głową, ale w przypadku małych obiektów może to mieć znaczenie.

Zauważ też, że w niektórych przypadkach kompilatory mogą zoptymalizować wirtualne wyszukiwanie, więc jest to tylko kara

Jak zawsze, jeśli chodzi o wydajność, zużycie pamięci itp .: Test porównawczy i profil oraz pomiary, porównanie wyników z alternatywami i sprawdzenie, gdzie spędza się NAJWIĘKSZY czas / pamięć, i nie próbuj optymalizować 90% kod, który nie jest uruchamiany zbyt często [większość aplikacji ma około 10% kodu, który ma duży wpływ na czas wykonywania i 90% kodu, który nie ma większego wpływu]. Zrób to na wysokim poziomie optymalizacji, aby kompilator miał już dobrą robotę! I powtórz, sprawdź ponownie i popraw krok po kroku. Nie staraj się być sprytnym i staraj się dowiedzieć, co jest ważne, a co nie, chyba że masz duże doświadczenie z danym rodzajem aplikacji.


1
„będzie narzutem w konstruktorze do tworzenia vtable” - zwykle vtable jest „tworzony” przez kompilator dla poszczególnych klas, przy czym konstruktor ma jedynie narzut przechowywania wskaźnika do budowanej instancji obiektu.
Tony

Ponadto ... Chodzi mi o unikanie przedwczesnej optymalizacji, ale odwrotnie, You **should** have a virtual destructor if your class is intended as a base-classjest to rażące uproszczenie - i przedwczesna pesymizacja . Jest to potrzebne tylko wtedy, gdy ktoś może usunąć klasę pochodną za pomocą wskaźnika do bazy. W wielu sytuacjach tak nie jest. Jeśli wiesz, że tak, to z pewnością poniesiesz koszty ogólne. Które, btw, jest zawsze dodawane, nawet jeśli rzeczywiste wywołania mogą być rozwiązane statycznie przez kompilator. W przeciwnym razie, kiedy właściwie kontrolujesz, co ludzie mogą zrobić z twoimi przedmiotami, nie warto
underscore_d
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.