Żeby zrozumieć istotę błędu trzeba mniej więcej wiedzieć jak budowany jest kod wynikowy w C++. Musimy wiedzieć co robi preprocesor z plikami h, jak powstają obj, co zawierają, co to są biblioteki statyczne, co robi linker z plikami obj i bibliotekami statycznymi. Więcej tutaj:
I jeszcze o sygnaturach:
- Function Signatures
- Is the return type part of the function signature?
- What exactly is a function signature?
- Name mangling
- Microsoft Visual C++ Name Mangling
Unresolved External Symbol
oznacza, że kompilator nie napotkał wcale (albo o odpowiedniej sygnaturze) eksportu definicji, dla której deklaracji wygenerował import. Przyczyn takiego błędu może być wiele.Czy ja się kiedyś doczekam takiej wersji Visual-a C++, który zasugeruje, że funkcja o takiej, a takiej nazwie jest zdefiniowana, ale jej sygnatura się różni od wymaganej. Albo jakaś możliwość podejrzenia jak wyglądają importowane i eksportowane sygnatury obj-tów i lib-ów. Większość błędów można by szybciej usunąć. Istotą tytułowego błędu jest to, że kompilator nic a nic nam nie pomoże w jego znalezieniu. Wszystko jest wypadkową naszego doświadczenia względem projektu, doświadczenia w ogóle i sprawdzenia wszystkiego tak by niczego nie przegapić. Niewdzięczny to błąd w istocie jest.
Po pierwsze: czytaj co jest generowane do
Output Windows
. Czasami można tam znaleźć użyteczne wskazówki.Rebuild
Czasami to wystarczy, niezależnie od wersji Visual-a, od czasy do czasu coś się mu popieprzyć lubi, tak już ma.
Brak definicji, niezgodność sygnatur
Jest deklaracja, brak jest definicji. Nazwa definicji różni się od nazwy deklaracji, lub różnią się listą parametrów. Mam tutaj na myśli błędy, które wprowadzamy sami podczas procesu pisania. I z reguły wiemy gdzie ich szukać.
Niezgodność sygnatur - trochę bardziej skomplikowana ewentualność
Np. plik "test1.h" zawiera definicję funkcji
void test(MY_TYPE t)
. Dołączamy go do test1.cpp
gdzie jest jego definicja i do test2.cpp
gdzie jest używany. Ale w obu plikach cpp
przed #include "test1.h"
tak mieszamy, że MY_TYPE dla obu plików cpp
jest różny.Niezgodność definicji preprocesora
W zasadzie jest to odmiana poprzedniego błędu. Z tym, że różnice w sygnaturach wywoływane są przez różne ustawienia opcji preprocesora w opcjach powiązanych projektów. Niektóre biblioteki statyczne mogą być na tyle skomplikowane, że wymagają konfiguracji, z reguły odbywa się to przez jeden plik nagłówkowy o np. wymownej nazwie
config.h
, a czasami przez ustawienie odpowiednich definicji preprocesora w opcjach projektu.Definicja jest wyłączona z procesu kompilacji
Może się też zdarzyć, że w wyniku ustawień preprocesora, dana funkcja nie zostanie skompilowana (np. zostanie wyłączona poprzez
#ifdef
).Plik lub projekt wyłączony jest z procesu budowania
Co do projektu sprawdź opcje solucji. Jeśli z procesu budowania wyłączony jest jeden plik będzie on oznaczony w specjalny sposób. Zmień jego właściwości ewentualnie. Plik może być też wyłączony z procesu budowania, gdyż nie został dodany do projektu.
Odpowiednia biblioteka statyczna nie jest dołączona
Jeśli nasza funkcja jest zadeklarowana w innej bibliotece statycznej albo DLL-ce sprawdź czy odpowiednia biblioteka statyczna jest dołączona w opcjach projektu
Properties|Linker|Input|Additional Dependencies
lub dla VS2010 Properties|Framework And References
. Niektóre projekty korzystają z
#pragma comment(lib, ...)
zwalniając nas tym samym z ustawiania różnych lib-ów w zależności od opcji kompilacji, np. debug i release, ansi i unicode, itp. Jedyne co musimy ustawić to ścieżkę do katalogu z lib-ami. Zgodność konfiguracji projektów dla konfiguracji solucji
Po pierwsze wielokrotnie zdarzało mi się, że konfiguracje rozjeżdżały się, szczególnie podczas dodawania i usuwania konfiguracji. Sprawdź np.: że w opcjach solucji dla konfiguracji solucji
debug
, wszystkie projekty mają wybrane debug
. Podobny problem tyczy się platformy. No i czasami także checkbox-y Build
mogą się pomieszać. Tego rodzaju błędy są w VS2010. Parę razy musiałem ręcznie edytować pliki projektu, gdyż GUI się sypało.Po drugie ustawiając np. konwencje wywołań projektu upewnij się, że robisz to dla wszystkich konfiguracji. Domyślnie modyfikujesz opcje wybranej konfiguracji, a z reguły nie o to nam chodzi.
Zmienna lub metoda globalna jest zadeklarowana jako static.
Słowo kluczowe
static
w tym wypadku oznacza dostępne tylko z danej jednostki kompilacji. Funkcja lub zmienna nie jest wystawiana na zewnątrz pliku obj
.Funkcje globalne szablonowe.
Zarówno deklaracja jak i definicja funkcji szablonowej musi się znajdować podczas linkowania w tej samej jednostce
obj
. Taki kod jest poprawny:template <class T> void test(T t); void main() { test(5); } template <class T> void test(T t) { }Ale nie praktyczny. Zdefiniowanie funkcji
test()
w innym pliku cpp
i dołączenie jej definicji poprzez plik nagłówkowy spowoduje błąd linkowania. A ponieważ funkcje szablonowe tworzy się po to by wykorzystywać je wiele razy musimy dołączyć definicję funkcji szablonowej do jej deklaracji. I takiego zapisu powinniśmy się zawsze trzymać.Gdybyśmy tak postąpili względem zwykłej globalnej funkcji możemy dostać błąd linkera, jeśli plik nagłówkowy z funkcją dołączymy do dwóch różnych plików
cpp
. Wyjątkiem są tutaj funkcje inline
. Dlaczego nic takiego nie dzieje w przypadku funkcji szablonowej ? Bo to jest szablon funkcji, ona w tym miejscu nie istnieje w postaci kodu. Klasy szablonowe, metody klas szablonowe.
To samo odnosi się zarówno do metod statycznych klasy i jak i metod obiektów. Zarówno kiedy te metody są szablonowe jak i metoda zawiera parametry szablonowe klasy, albo to i to jednocześnie. Definicja i deklaracja powinna być w tym samym miejscu.
Zmienne statyczne klasy
class test_t1 { public: static int x; };To nie wystarczy. Zapisaliśmy jakby:
extern test_t1::x
Musimy więc dopisać:
int test_t1::x = 5;
Zmiennej nie trzeba inicjalizować. A definicję tą powinniśmy zamieścić w pliku
cpp
z takich samych powodów jak dla zwykłych zmiennych.Mieszanie kodu C++ i C
Visual C++ niezależnie od rozszerzenia pliku może go skompilować jako C albo jako C++. Dla każdego pliku z osobna i dla każdego projektu możemy ustawić
Properies|C++|Advanced|Compile As
. Mamy trzy możliwości kompiluj zgodnie z rozszerzeniem, kompiluj jako C i kompiluj jako C++. Jeszcze się nie spotkałem, żeby ktoś ustawił tą opcję dla pliku z osobna (jak i inne wspomniane w artykule). Generalnie nie powinno się tego robić. Co jeśli plików są tysiące, co jeśli autor błędu siedzi obok mnie, co jeśli ty nim jesteś, to może się źle skończyć.
Funkcje C i C++ mają różne sygnatury. Kompilujemy kod C++, kompilator napotyka na definicję funkcji, generuje w
obj
import dla funkcji C++. Tymczasem eksport jest typu C. Musimy kompilatorowi powiedzieć, że wspomniana funkcją ma eksport typu C. Np. w taki sposób: #ifdef __cplusplus #define MY_EXTERN extern "C" #else #define MY_EXTERN #endifWtedy w pliku nagłówkowym możemy zapisać:
MY_EXTERN void test();
Jeśli wspomniany plik nagłówkowy jest dołączany do pliku C, całe
extern "C"
jest pomijane, gdyż taki zapis C rozpoznaje jako błędny, funkcja jest eksportowana z sygnaturą C. Dla pliku cpp
definiowany jest automatycznie symbol __cplusplus
i funkcja jest oznaczana jako extern "C"
, czyli importowana jest z sygnaturą C.Konwencja wywołania
Ustawiana w opcjach projektu
Properies|C++|Advanced|Calling Convention
. Ich zgodność lub niezgodność dla kilku powiązanych projektów niczego nie oznacza, musisz upewnić się czy sporne funkcje nie zostały oznaczone konkretną konwencją wywołania. Jeśli konwencja jest zdefiniowana dyrektywą preprocesora np. NAZWA_PORJEKTU_EXTERN warto spojrzeć co się za tym kryje.Treat wchar_t as Built-in Type
Ustawiane w opcjach projektu
Properies|C++|Language|Treat wchar_t as Built-in Type
. Tak jak poprzednio sprawdzamy zgodność dla powiązanych projektów. Jeśli wszystkie nasze nierozwiązane funkcje zawierają w sobie parametry typu wchar_t
albo jako unsigned short
może to wskazywać na ten rodzaj błedu.DLL
Linkowanie z DLL-ką jest specyficzne dla linkera, a później dla systemu, który aplikację uruchamia. Musimy w sposób specjalny powiedzieć kompilatorowi, że ta funkcja znajduje się w DLL-ce. Dla eksportu używamy
__declspec(dllexport)
, zaś dla importu __declspec(dllimport)
. Dekoracji takiej możemy także użyć w stosunku do klas. Metody klasy to odpowiednio nazwane funkcje biblioteczne, których jednym z parametrów jest wskaźnik na obiekt.Z reguły mamy plik nagłówkowy z definicjami eksportowanych/importowanych funkcji. Niestety Visual C++ nie jest tutaj zbyt domyślny. Jeśli nasz drugi projekt to plik wykonywalny, ciągle domaga się definiowania importu, tak jakby dało się eksportować funkcję z exe-ca. Rozwiązanie powszechnie stosowane to dodanie na początku takiego pliku nagłówkowego czegoś takiego:
#ifdef LIBRARY_EXPORTS # define LIBRARY_API __declspec(dllexport) #else # define LIBRARY_API __declspec(dllimport) #endifMusimy więc dodać do definicji preprocesora do naszego projektu
LIBRARY_EXPORTS
.Specyficzny błąd dla VC2010
VC2010 pozwala w opcjach projektu ustawić inne projekty od których on zależy. Dzięki temu będą one kompilowane przed naszym projektem. Jeśli są to lib-y możemy zaznaczyć ich włączenie do naszego projektu. Znacznie upraszcza do zarządzanie dużą solucją. Problem który napotkałem można streścić tak: dla niektórych lib-ów, jeśli lib A włączamy do referencji lib-u B (zaznaczamy opcję
Use library dependency inputs
, ten zaś do exe-ka C, to mamy błąd. Jeśli do C dołączymy A to błędu nie ma. Nie wiem o co chodziło, nie umiałem tego rozgryźć.Podsumowanie
To zapewne nie wszystko, w opcjach linkera jest jeszcze parę ustawień, które mogą zamieszać, jednakże jako, że nigdy nie miałem z nimi problemu nie opisałem ich.
Brak komentarzy:
Prześlij komentarz