Co jest vtable
?
Przed próbą naprawienia warto wiedzieć, o czym mówi komunikat o błędzie. Zacznę od wysokiego poziomu, a potem dopracuję więcej szczegółów. W ten sposób ludzie mogą przejść do przodu, gdy tylko poczują się dobrze ze zrozumieniem tabel. … I jest teraz grupa ludzi skaczących przed siebie. :) Dla osób pozostających w pobliżu:
Vtable jest w zasadzie najczęstszą implementacją polimorfizmu w C ++ . Kiedy używane są vtables, każda klasa polimorficzna ma gdzieś w programie vtable; możesz myśleć o tym jako o (ukrytym) static
elemencie danych w klasie. Każdy obiekt klasy polimorficznej jest powiązany z tabelą vt dla swojej najbardziej pochodnej klasy. Sprawdzając to powiązanie, program może działać na swoją magię polimorficzną. Ważne zastrzeżenie: vtable to szczegół implementacji. Nie jest to wymagane przez standard C ++, nawet jeśli większość (wszystkich?) Kompilatorów C ++ używa vtables do implementacji zachowania polimorficznego. Przedstawione przeze mnie szczegóły są typowymi lub rozsądnymi podejściami. Kompilatory mogą się od tego różnić!
Każdy obiekt polimorficzny ma (ukryty) wskaźnik do tabeli vt dla najbardziej pochodnej klasy obiektu (być może wielu wskaźników, w bardziej złożonych przypadkach). Patrząc na wskaźnik, program może stwierdzić, jaki jest „prawdziwy” typ obiektu (z wyjątkiem budowy, ale pomińmy ten szczególny przypadek). Na przykład, jeśli obiekt typu A
nie wskazuje na vtable A
, wówczas ten obiekt jest w rzeczywistości podobiektem czegoś, z czego pochodzi A
.
Nazwa „vtable” pochodzi od „ v rzeczywistej tabeli funkcji ”. Jest to tabela przechowująca wskaźniki do funkcji (wirtualnych). Kompilator wybiera konwencję dotyczącą układu tabeli; prostym podejściem jest przejście przez funkcje wirtualne w kolejności, w jakiej są zadeklarowane w definicjach klas. Po wywołaniu funkcji wirtualnej program podąża za wskaźnikiem obiektu do tabeli vt, przechodzi do pozycji powiązanej z żądaną funkcją, a następnie używa wskaźnika funkcji zapisanej do wywołania poprawnej funkcji. Jest wiele różnych sztuczek, żeby to zrobić, ale nie będę się tutaj zajmował.
Gdzie / kiedy jest vtable
generowany?
Vtable jest automatycznie generowany (czasem nazywany „emitowanym”) przez kompilator. Kompilator może emitować vtable w każdej jednostce tłumaczenia, która widzi definicję klasy polimorficznej, ale zwykle byłoby to niepotrzebne przesadne działanie. Alternatywą ( używaną przez gcc i prawdopodobnie przez innych) jest wybranie pojedynczej jednostki tłumaczeniowej, w której umieści się vtable, podobnie jak w przypadku wybrania pojedynczego pliku źródłowego, w którym można umieścić statyczne elementy danych klasy. Jeśli podczas tego procesu wyboru nie zostaną wybrane żadne jednostki tłumaczeniowe, tabela vt staje się niezdefiniowanym odwołaniem. Stąd błąd, którego przesłanie nie jest wprawdzie szczególnie jasne.
Podobnie, jeśli proces selekcji powoduje wybór jednostki tłumaczeniowej, ale plik obiektowy nie jest udostępniany konsolidatorowi, wówczas tabela vt staje się niezdefiniowanym odwołaniem. Niestety komunikat o błędzie może być w tym przypadku jeszcze mniej wyraźny niż w przypadku, gdy proces wyboru nie powiódł się. (Dzięki autorom odpowiedzi, którzy wspominali o tej możliwości. Prawdopodobnie w innym przypadku o tym zapomniałbym.)
Proces selekcji używany przez gcc ma sens, jeśli zaczniemy od tradycji poświęcania (pojedynczego) pliku źródłowego każdej klasie, która potrzebuje jednej do jego implementacji. Byłoby miło wyemitować vtable podczas kompilacji tego pliku źródłowego. Nazwijmy to naszym celem. Proces selekcji musi jednak działać, nawet jeśli nie przestrzega się tej tradycji. Zamiast szukać implementacji całej klasy, spójrzmy na implementację konkretnego członka klasy. Jeśli przestrzega się tradycji - a ten członek jest rzeczywiście wdrażany - osiąga to cel.
Element wybrany przez gcc (i potencjalnie przez inne kompilatory) jest pierwszą nieliniową funkcją wirtualną, która nie jest czysto wirtualna. Jeśli należysz do tłumu, który deklaruje konstruktory i destruktory przed innymi funkcjami składowymi, to ten destruktor ma dużą szansę zostać wybrany. (Pamiętasz, jak zrobić wirtualny destruktor, prawda?) Istnieją wyjątki; Spodziewałbym się, że najczęstszymi wyjątkami są sytuacje, gdy dla destruktora wprowadzona jest definicja wbudowana i gdy żądany jest domyślny destruktor (użycie „ = default
”).
Bystry może zauważyć, że klasa polimorficzna może dostarczać wbudowane definicje wszystkich swoich funkcji wirtualnych. Czy to nie powoduje niepowodzenia procesu selekcji? Robi to w starszych kompilatorach. Czytałem, że najnowsze kompilatory rozwiązały tę sytuację, ale nie znam odpowiednich numerów wersji. Mógłbym spróbować to sprawdzić, ale łatwiej jest albo go zakodować, albo poczekać, aż kompilator narzeka.
Podsumowując, istnieją trzy kluczowe przyczyny błędu „niezdefiniowane odwołanie do vtable”:
- Funkcja członka nie ma swojej definicji.
- Plik obiektowy nie jest łączony.
- Wszystkie funkcje wirtualne mają wbudowane definicje.
Przyczyny te same w sobie nie są wystarczające, aby samodzielnie spowodować błąd. Zamiast tego należy rozwiązać problem. Nie oczekuj, że celowe utworzenie jednej z tych sytuacji z pewnością spowoduje ten błąd; są inne wymagania. Spodziewaj się, że rozwiązanie tych sytuacji rozwiąże ten błąd.
(OK, liczba 3 mogła być wystarczająca, gdy zadano pytanie).
Jak naprawić błąd?
Witaj z powrotem ludzi, którzy skaczą do przodu! :)
- Spójrz na swoją definicję klasy. Znajdź pierwszą nieliniową funkcję wirtualną, która nie jest czysto wirtualna (nie „
= 0
”) i której definicję podasz (nie „ = default
”).
- Jeśli nie ma takiej funkcji, spróbuj zmodyfikować swoją klasę, aby była taka. (Prawdopodobnie błąd został rozwiązany.)
- Zobacz także odpowiedź Philipa Thomasa na ostrzeżenie.
- Znajdź definicję tej funkcji. Jeśli go brakuje, dodaj go! (Prawdopodobnie błąd został rozwiązany.)
- Sprawdź polecenie link. Jeśli nie wspomina o pliku obiektowym z definicją tej funkcji, napraw to! (Prawdopodobnie błąd został rozwiązany.)
- Powtarzaj kroki 2 i 3 dla każdej funkcji wirtualnej, a następnie dla każdej funkcji innej niż wirtualna, aż błąd zostanie rozwiązany. Jeśli nadal utkniesz, powtórz dla każdego statycznego elementu danych.
Przykład
Szczegółowe informacje na temat tego, co należy zrobić, mogą się różnić, a czasem rozgałęzić na osobne pytania (np. Co to jest niezdefiniowany błąd odniesienia / nierozwiązany błąd symbolu zewnętrznego i jak to naprawić? ). Podam jednak przykład tego, co należy zrobić w konkretnym przypadku, który może być kłopotliwy dla nowszych programistów.
Krok 1 wspomina o modyfikacji klasy, tak aby miała ona funkcję określonego typu. Jeśli opis tej funkcji przeszedł ci przez głowę, być może znajdujesz się w sytuacji, którą zamierzam rozwiązać. Pamiętaj, że jest to sposób na osiągnięcie celu; nie jest to jedyny sposób, a w twojej konkretnej sytuacji mogą być łatwiejsze sposoby. Zadzwońmy do twojej klasy A
. Czy twój destruktor jest zadeklarowany (w definicji klasy) jako jeden z nich?
virtual ~A() = default;
lub
virtual ~A() {}
? Jeśli tak, dwa kroki zmienią twój destruktor w pożądany typ funkcji. Najpierw zmień tę linię na
virtual ~A();
Po drugie, wstaw następujący wiersz do pliku źródłowego, który jest częścią twojego projektu (najlepiej plik z implementacją klasy, jeśli taki masz):
A::~A() {}
To sprawia, że Twój (wirtualny) destruktor nie jest wbudowany i nie jest generowany przez kompilator. (Zachęcamy do modyfikowania elementów, aby lepiej pasowały do stylu formatowania kodu, takich jak dodawanie komentarza nagłówka do definicji funkcji).