2010-07-20

Visual C++ 2010 - wyrażenia Lambda

Wyrażenie lambda w C++ pozwala nam stworzyć obiekt (zmienną), który jest funkcją. Istnieje tutaj duże podobieństwo do wyrażeń Lambda z C#, które są tak naprawdę anonimowymi delegatami, a te są zwykłymi delegatami, które kompilator tworzy podczas generowania kodu. Można to zaobserwować używając narzędzia Reflector. Podobnie jest w wyrażeniami Lambda w C++. każde wyrażenie lambda jest tak naprawdę strukturą, która przeładowuje operator(). Mając do dyspozycji wyrażenia Lambda chcąc wywołać jakąś funkcję wymagającą jako parametru obiektu funkcyjnego nie musimy tworzyć odpowiedniej struktury albo klasy, tylko możemy zapisać to w jednej linijce kodu definiując wyrażenie Lambda jako parametr funkcji. Wyrażenia Lambda pojawiły się w VC2010, jako efekt tworzenia się nowego standardu C++0x. VC2010 implementuje wiele z elementów tego standardu, w tym wyrażenia Lambda.

Przykład prostego wyrażenia lambda:

auto lmb1 = [] (int a) { return a + 2; };int r1 = lmb1(6)

Słowo kluczowe auto to też nowy element C++0x. Jest on odpowiednikiem var z C#. Kompilator sam w tym przypadku ustali typ zmiennej. Auto pozwala na uproszczenie kodu, zwłaszcza jeśli jeśli jest to jakaś skomplikowana klasa szablonowa, której pełnej definicji nawet możemy nie poznać bez wgłębienia się w kod. Poza tym w tym przypadku nie mamy innej możliwości, typ jest nam nieznany, jest to anonimowa struktura wygenerowana przez kompilator:

'anonymous-namespace'::<lambda0>

Nasze wyrażenie lambda, o którym możemy myśleć jako o funkcji zwiększa liczbę o dwa. I teraz jeszcze jeden przykład:

int r2 = [] (int a) { return a + 2; }(6);

Blok [] to z dokumentacji MSDN lambda-parameter-declaration-list (referred to as parameter list later in this topic). Za polską wikipedią to domknięcie. Ja będę go nazywać blokiem przechwytującym. Więcej o tym dalej.

Za blokiem przechwytującym w nawiasach mamy listę parametrów wyrażenia Lambda. I tutaj mamy trzy ograniczenia: nie ma parametrów domyślnych, zmiennej liczby parametrów i parametrów nie nazwanych. W przypadku gdy nasza funkcja nie ma parametrów () możemy pominąć.

Dalej w nawiasach klamrowych znajduje się dowolny kod funkcji.

Powyższe wyrażenie zwraca int. Typ zwracany możemy zdefiniować bezpośrednio, albo jak wyżej pozostawić to domyślności kompilatora.
Lambda nie musi koniecznie zwracać wyniku:

[] (int a) { a + 2; }(4);

W tym wypadku zwracany jest void.

Co w takim wypadku:

auto L3 = [] (int x) { return (x % 2 == 0) ? (x * 2) : (x / 2.0); };

Wynikiem wyrażenia Lambda będzie int gdyż on został napotkany podczas kompilacji jako pierwszy. Double podlega automatycznej konwersji do int więc nie ma tutaj żadnego błędu. Osobiście nie podoba mi się takie zachowanie, według mnie taka niejednoznaczność powinna wygenerować błąd kompilacji i zmusić użytkownika do podania zwracanego rezultatu, a robi się to tak:
auto L3 = [] (int x) -> double { return (x % 2 == 0) ? (x * 2) : (x / 2.0); };
Jeszcze jeden przykład:

auto L2 = [](int x) { x = x + 1; return x; };

Tutaj też mamy błąd. Kompilator założył po analizie pierwszej linii, że funkcja zwraca void. Czyli tak naprawdę nie podawanie typu wyniku jest zarezerwowane dla najprostszych funkcji.

Kolejny błędny przykład:

auto L2 = [](int x) 
{ 
    if (x ==2) 
        return 3;
    else 
    { 
        if (x == 3) 
            return x + 2; 
        else
            return 5; 
    }
};

W tym wypadku ponieważ każda ścieżka przepływu zwraca int-a wydaje się, że kompilator powinien się domyśleć zwracanego wyrażenia.

Kolejny blok throw() o którym tylko wspomnę:

auto L3 = [] (int x) throw() -> int { test(); return (x % 2 == 0) ? (x * 2) : throw 4; };

Takie wyrażenie jest błędne, zadeklarowaliśmy, że Lambda nie będzie wyrzucać wyjątków, a to robi. Więcej informacji: Exception Specifications

Do omówienia pozostał nam jeszcze blok przechwytujący. Funkcja Lambda w swoim zasięgu ma tylko zmienne i funkcje globalne (w tym statyczne klas).

Przykład:

class Test
{
    private:    

        static int w;
        int z;

    public:

        void test()
        {
            auto L2 = [](int x) { return x + z; };
            auto L3 = [](int x) { return x + w; };
        }
};

int w;

void  main()
{ 
    int z;
    auto L2 = [](int x) { return x + z; };

    int* p = new int[5];
    auto L4 = [](int x) { return x + p[1]; };

    auto L3 = [](int x) { return x + w; };
};

Poprawne są tylko wyrażenia starające się odwołać do zmiennych w. Wyrażenie lambda to anonimowa struktura, jej operator(), który zawiera ciało naszej funkcji nie ma dostępu do nie-globalnych składników. Chcąc wykorzystać w wyrażeniu Lambda zmienne nie-globalne musimy je przechwycić. Są one przechwytywane do pól lokalnych struktury podczas konstruowania jej.

Przechwytywać możemy przez wartość i przez referencję:

int z;
int* p = new int[5];

auto L3 = [&](int x) { return x + z + p[1]; };
auto L4 = [&, z](int x) { return x + z + p[1]; };
//auto L5 = [&, &z](int x) { return x + z + p[1]; }; // Błąd
auto L5 = [&, z, p](int x) { return x + z + p[1]; };
auto L6 = [=](int x) { return x + z + p[1]; };
//auto L7 = [=, p](int x) { return x + z + p[1]; }; // Błąd
auto L8 = [=, &p](int x) { return x + z + p[1]; };
auto L9 = [=, &p, &z](int x) { return x + z + p[1]; };
auto L10 = [&p, z](int x) { return x + z + p[1]; };

Linia L3 Przechwytuje wszystko jako referencje zmiennych, zaś linia L6 przechwytuje wszystko jako wartości. Pozostałe przypadki są kombinacją przechwytywania przez referencje i wartość. Np. linia L5 przechwytuje wszystko przez referencje, za wyjątkiem dwóch innych zmiennych. W linii L10 trzeba podać wszystkie zmienne przechwytywane. Przypadki błędne to swego rodzaju tożsamości, pierwszy błąd mówi przechwyć wszystko przez referencje i z przez referencję.

Tak przechwytuje się dostęp do składników klasy:

class Test
{
    private:    

        int z;

        void test_priv()
        {
        }

    public:

        void test()
        {
            auto L2 = [this](int x) -> int { test_priv(); return x + z; };
        }
};

Przy okazji widzimy, że wyrażenie Lambda zdefiniowane we wnętrzu metody klasy ma dostęp do jego składników prywatnych (staje się przyjacielem klasy). Taki sam efekt daje zastosowanie [&] albo [=]. [&] oznacza, że this łapiemy przez wartość. Zapisanie [&this] jest błędem.

Jaka jest różnica pomiędzy przechwyceniem przez wartość i przez referencję?

void main()
{
    byte z = 0;
    byte* p = new byte[5];
    memset(p, 0, 5);
    byte* cp = p;
    printf("z: %d, p: %08X, cp: %08X, p[1]: %d, cp[1]: %d\n", 
            z, p, cp, p[1], cp[1]);
    auto L3 = [=](int x) mutable -> int { z = x; p[1] = x; p = new byte[5]; 
        memset(p, 11, 5); return x + z + p[1]; };
    L3(5);
    printf("z: %d, p: %08X, cp: %08X, p[1]: %d, cp[1]: %d\n", 
            z, p, cp, p[1], cp[1]);
    auto L4 = [&](int x) -> int { z = x; p[1] = x; p = new byte[5]; 
        memset(p, 12, 5); return x + z + p[1]; };
    L4(6);
    printf("z: %d, p: %08X, cp: %08X, p[1]: %d, cp[1]: %d\n", 
            z, p, cp, p[1], cp[1]);
};

Wyniki:

z: 0, p: 00251370, cp: 00251370, p[1]: 0, cp[1]: 0
z: 0, p: 00251370, cp: 00251370, p[1]: 5, cp[1]: 5
z: 6, p: 00251400, cp: 00251370, p[1]: 12, cp[1]: 6


Słowo mutable pozwala nam na zmienianie parametrów przechwyconych przez wartość.

Widzimy więc, że L3 operuje na kopiach, zaś L4 na referencjach.

Przechwycenie następuje w momencie zdefiniowania wyrażenia Lambda, nie jego wykonania, takie wyrażenie posiada swój stan, który może się zmieniać.

int z = 0;
auto L1 = [=] ()  { return z; };
z++;
auto L2 = [=] ()  { return z; };
z++;
int a1 = L1();
z++;
int a2 = L2();

Wyniki to a1 = 0 i a2 = 1.

int z = 0;
auto L1 = [&] ()  { return z; };
z++;
auto L2 = [&] ()  { return z; };
z++;
int a1 = L1();
z++;
int a2 = L2();

Wyniki to a1 = 2 i a2 = 3.

Przechwytując referencje albo jakiś wskaźnik zawsze trzeba uważać bo można łatwo coś namieszać. Staje się to tym ważniejsze jeśli wyrażenie Lambda zostanie zrównoleglone w czymś w rodzaju for_each_parallel.

Poza tym warto zauważyć, że definicja wyrażenia Lambda nie oznacza jego wykonania. Mówimy tutaj o opóźnionym wykonaniu, wyrażenie jest wykonywane dopiero w momencie gdy jest potrzebne. W momencie definiowania tworzona jest struktura, która stanowi obudowę wyrażenia Lambda i przechwytywane są zmienne do tej struktury.

Opóźnione wykonywanie rodzi tutaj problemy. Np. skasowanie p przed wywołaniem L3 spowoduje błąd. Czyli pierwszy rodzaj kłopotów to trwałość zmiennych alokowanych dynamicznie, a przechwyconych do wyrażenia Lambda i skasowanych przed wywołaniem wyrażenia Lambda. W językach z GC nie mamy takich problemów.

Drugi rodzaj możliwych błędów to zmienienie stanu obiektów przechwyconych przed wywołaniem wyrażenia Lambda, np. zamknięcie pliku.

Różnorakich błędów może być mnóstwo, trzeba po prostu uważać.

Spójrzmy na poniższy przykład zastosowania wyrażenia Lambda:

int _tmain(int argc, _TCHAR* argv[])
{
    vector<int> v;
    for (int i = 0; i < 10; ++i) 
        v.push_back(i);
    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; } );

 return 0;
}

Przekazaliśmy do funkcji for_each jako parametr wyrażenie Lambda, która jest anonimowo zdefiniowaną strukturą. Tak naprawdę for_each jest funkcją szablonową i spodziewa się, że trzeci parametr ma przeładowany operator (). Co gdybyśmy chcieli zwrócić jako wynik funkcji wyrażenie Lambda. W C# możemy do tego wykorzystać delegaty, najlepiej w postaci Action i Func. W C++ mamy odpowiednik function.

using namespace std;
using namespace std::tr1;

void round(function<int(double)> round_impl)
{
    double x = 0.1;
    int y = round_impl(0.1);
}

int _tmain(int argc, _TCHAR* argv[])
{
    auto round_impl = [&] (double g ) { return (int)(g + 0.5); };
    function<int(double)> f = round_impl;

    round(round_impl);

 return 0;
}

Użycie function zwiększa naszą elastyczność, pozwalając nam przekazywać funkcje jako parametry, ale kosztem wydajności. Teraz na koniec możemy spóbować co się stanie jeśli przechwycimy przez referencję zmienną lokalną funkcji i spróbujemy jej użyć po wyjściu z jej zakresu w wyrażeniu Lambda.

function<int()> CreateF()
{
    int x = 111;
    return [&] () -> int { x++; return x; };
}

int _tmain(int argc, _TCHAR* argv[])
{
    int y = CreateF()();
    return 0;
}

Wartość zmiennej y jest nieustalona, samo wyrażenie Lambda modyfikuje pamięć poza wskaźnikiem stosu. W pewnych okolicznościach może to prowadzić do losowych i fatalnych błędów. I jeszcze jeden przykład:

[](){}();
[]{}();
function(5)();


Skompiluje się, ale podczas działania ostatni przykład wyrzuci wyjątek.

Podsumowanie

Wszystko co możemy zrobić za pomocą wyrażeń Lambda mogliśmy zrobić wcześniej używając obiektów funkcyjnych. Z tym, że teraz możemy to zrobić prościej i szybciej. A z tym wiąże się fakt: jeśli coś jest ciężkie do wykorzystania to się z reguły z tego nie skorzysta, bo się nie chce tyle pisać, zaciemniać kod, goni nas czas. Innymi słowy wyrażenia Lambda przybliżają nas do tego by programować bardziej funkcyjnie.

Korzystałem z:

Lambda Expressions in C++
Function Objects
Lambdas, auto, and static_assert: C++0x Features in VC10, Part 1
C++0x - Wikipedia Lambda Expression Syntax
Examples of Lambda Expressions

2010-07-19

Visual C++ - symbol already defined, one or more multiply defined symbols found

Wpis będzie dotyczyć Visual C++ 2010, ale prawie na pewno wszystko odnosi się też do poprzednich wersji Visual-a.

Oba tytułowe błędy występują zawsze razem.

Żeby zrozumieć istotę tych błędów 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:
Poniżej lista przyczyn, które do tytułowych błędów prowadzą. Nie jest ona pewnie kompletna, ale mam nadzieje, że zawiera wszystkie najpopularniejsze przyczyny.

Poniższe przypadki odnoszą się do błędów związanych z symbolami w naszym kodzie, dalej są omówione przypadki błędów związanych z symbolami z bibliotek CRT i MFC.

Podwójna definicja symbolu (funkcja, zmienna)


Linker poszukuje eksportu symbolu dla powiązania go z jego importem i znajduje wiele eksportów o takiej samej syngaturze. Mogą się one znajdować w obj-tach, albo importowanych lib-ach. Po pierwsze możemy oznaczyć symbole używane tylko lokalnie w plikach cpp jako static, dzięki temu nie będą one eksportowane. Po drugie możemy zmienić nazwę spornych symboli.

Deklaracja symbolu w pliku nagłówkowym


Problem pojawia się kiedy taki plik nagłówkowych zostanie dołączony do conajmniej dwóch różnych plików cpp. Rozwiązanie to pozostawienie deklaracji symbolu w pliku nagłówkowym i przeniesienie definicji do pliku cpp. W przypadku funkcji możemy w ostateczności oznaczyć je jako inline.

Plik nagłówkowy dołączany jest wielokrotnie


Dopisz na początku pliku nagłówkowego #pragma once.

Brak #include <afx.h>


Teraz załóżmy, że nasza solucja składa się z projektu pliku wykonywalnego i dołączanej do niej biblioteki statycznej z innego projektu w solucji. Taka przykładowa solucja jest używana dalej w artykule.

Opcje projektów General|Character Set są zgodne. Jeśli do projektu pliku wykonywalnego nie dołączymy afx.h to dostaniemy błędy w zależności od Character Set:

3>uafxcwd.lib(afxmem.obj) : error LNK2005: "void * __cdecl operator new(unsigned int)" (??2@YAPAXI@Z) already defined in LIBCMTD.lib(new.obj)
3>uafxcwd.lib(afxmem.obj) : error LNK2005: "void __cdecl operator delete(void *)" (??3@YAXPAX@Z) already defined in LIBCMTD.lib(dbgdel.obj)

2>nafxcwd.lib(afxmem.obj) : error LNK2005: "void * __cdecl operator new(unsigned int)" (??2@YAPAXI@Z) already defined in LIBCMTD.lib(new.obj)
2>nafxcwd.lib(afxmem.obj) : error LNK2005: "void __cdecl operator delete(void *)" (??3@YAXPAX@Z) already defined in LIBCMTD.lib(dbgdel.obj)


Błędów może być mniej albo więcej, zależy ile potencjalnie konfliktowych symboli użyliśmy. Oczywiście aby uzyskać takie błędy projekt pliku wykonywalnego nie może korzystać z MFC.

Niezgodność opcji projektów


Podejrzane opcje projektów to General|Use of MFC, General|Character Set i C++|Code Generation|Runtime Library. W zależności od ich kombinacji błędy będą różne. Oto niektóre podejrzane komunikaty:

1>LINK : warning LNK4098: defaultlib 'nafxcwd.lib' conflicts with use of other libs; use /NODEFAULTLIB:library

1>msvcrtd.lib(ti_inst.obj) : error LNK2005: "private: __thiscall type_info::type_info(class type_info const &)" (??0type_info@@AAE@ABV0@@Z) already defined in LIBCMTD.lib(typinfo.obj)
1>msvcrtd.lib(ti_inst.obj) : error LNK2005: "private: class type_info & __thiscall type_info::operator=(class type_info const &)" (??4type_info@@AAEAAV0@ABV0@@Z) already defined in LIBCMTD.lib(typinfo.obj)
1>LINK : warning LNK4098: defaultlib 'msvcrtd.lib' conflicts with use of other libs; use /NODEFAULTLIB:library

1>LINK : warning LNK4098: defaultlib 'mfc100u.lib' conflicts with use of other libs; use /NODEFAULTLIB:library
1>LINK : warning LNK4098: defaultlib 'mfcs100u.lib' conflicts with use of other libs; use /NODEFAULTLIB:library
1>LINK : warning LNK4098: defaultlib 'msvcrt.lib' conflicts with use of other libs; use /NODEFAULTLIB:library


Dla uniknięcia tego rodzaju błędów wspomniane wcześniej opcje powinny zostać ujednolicone.

Trochę więcej informacji na temat dwóch ostatnich przyapdków A LNK2005 error occurs when the CRT library and MFC libraries are linked in the wrong order in Visual C++

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.