2010-07-18

Unresolved External Symbol w Visual C++

Bardzo powszechny błąd podczas tworzenia programów w C++. Tutaj skupie się na Visual C++, konkretnie w wersji 2010, choć prawie wszystko będzie się odnosić również do poprzednich wersji.

Ż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:
Błąd 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
#endif
Wtedy 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)
#endif
Musimy 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