25,5 — Wczesne wiązanie i późne wiązanie

W tej i następnej lekcji przyjrzymy się bliżej sposobowi implementacji funkcji wirtualnych. Chociaż informacje te nie są absolutnie niezbędne do efektywnego korzystania z funkcji wirtualnych, są interesujące. Niemniej jednak obie sekcje można traktować jako opcjonalne.

Gdy program C++ jest wykonywany, jest on wykonywany sekwencyjnie, zaczynając od góry main(). Kiedy zostanie napotkane wywołanie funkcji, punkt wykonania przeskakuje na początek wywoływanej funkcji. Skąd procesor wie, jak to zrobić?

Gdy program jest kompilowany, kompilator konwertuje każdą instrukcję w programie C++ na jedną lub więcej linii języka maszynowego. Każda linia języka maszynowego ma swój własny, unikalny adres sekwencyjny. Nie inaczej jest w przypadku funkcji — napotkana funkcja jest konwertowana na język maszynowy i przypisywany jest następny dostępny adres. W ten sposób każda funkcja kończy się unikalnym adresem.

Wiązanie i wysyłanie

Nasze programy zawierają wiele nazw (identyfikatory, słowa kluczowe itp.). Każda nazwa ma zestaw powiązanych właściwości: na przykład, jeśli nazwa reprezentuje zmienną, zmienna ta ma typ, wartość, adres pamięci itp…

Na przykład, gdy mówimy int x, mówimy kompilatorowi, aby powiązał nazwę x z typem int. Później, jeśli powiemy x = 5, kompilator może użyć tego powiązania, aby wpisać i sprawdzić przypisanie, aby upewnić się, że jest ono prawidłowe.

W ogólnym programowaniu wiązanie to proces kojarzenia nazw z takimi właściwościami. wiązanie funkcji (lub wiązanie metody) jest procesem która określa, jaka definicja funkcji jest powiązana z wywołaniem funkcji. Proces faktycznego wywoływania powiązanej funkcji nazywa się wysyłaniem.

W C++ termin wiązanie jest używany bardziej swobodnie (a wysyłanie jest zwykle uważane za część wiązania). Poniżej przeanalizujemy użycie tych terminów w C++.

Nomenklatura

Wiązanie jest terminem przeciążonym. W innych kontekstach wiązanie może odnosić się do:

  • Powiązania odniesienia do obiektu
  • std::bind
  • Powiązania językowego

Wczesnego wiązania

Większość wywołań funkcji, które napotka kompilator, będzie bezpośrednimi wywołaniami funkcji. Bezpośrednie wywołanie funkcji to instrukcja, która bezpośrednio wywołuje funkcję. Na przykład:

#include <iostream>

struct Foo
{
    void printValue(int value)
    {
        std::cout << value;
    }
};

void printValue(int value)
{
    std::cout << value;
}

int main()
{
    printValue(5);   // direct function call to printValue(int)

    Foo f{};
    f.printValue(5); // direct function call to Foo::printValue(int)
    return 0;
}

W C++, gdy wykonywane jest bezpośrednie wywołanie funkcji niebędącej składową lub funkcji składowej niewirtualnej, kompilator może określić, która definicja funkcji powinna być dopasowana do wywołania. Nazywa się to czasem wczesnym wiązaniem (lub wiązaniem statycznym), ponieważ można je wykonać w czasie kompilacji. Kompilator (lub linker) może następnie wygenerować instrukcje w języku maszynowym, które każą procesorowi przejść bezpośrednio do adresu funkcji.

Dla zaawansowanych czytelników

Jeśli spojrzymy na kod asemblera wygenerowany dla wywołania printValue(5) (przy użyciu clang x86-64), zobaczymy coś takiego:

        mov     edi, 5           ; copy argument 5 into edi register in preparation for function call
        call    printValue(int)  ; directly call printValue(int)

Wyraźnie widać, że jest to bezpośrednie wywołanie funkcji printValue(int).

Wywołania to przeciążone funkcje i szablony funkcji można również rozwiązać w czasie kompilacji:

#include <iostream>

template <typename T>
void printValue(T value)
{
    std::cout << value << '\n';
}

void printValue(double value)
{
    std::cout << value << '\n';
}

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);   // direct function call to printValue(int)
    printValue<>(5); // direct function call to printValue<int>(int)

    return 0;
}

Przyjrzyjmy się prostemu programowi kalkulatora, który wykorzystuje wczesne wiązanie:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int subtract(int x, int y)
{
    return x - y;
}

int multiply(int x, int y)
{
    return x * y;
}

int main()
{
    int x{};
    std::cout << "Enter a number: ";
    std::cin >> x;

    int y{};
    std::cout << "Enter another number: ";
    std::cin >> y;

    int op{};
    std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
    std::cin >> op;

    int result {};
    switch (op)
    {
        // call the target function directly using early binding
        case 0: result = add(x, y); break;
        case 1: result = subtract(x, y); break;
        case 2: result = multiply(x, y); break;
        default:
            std::cout << "Invalid operator\n";
            return 1;
    }

    std::cout << "The answer is: " << result << '\n';

    return 0;
}

Ponieważ add(), subtract(), I multiply() są wszystkie bezpośrednie wywołania funkcji do funkcji nieelementowych, kompilator dopasuje te wywołania funkcji do odpowiednich definicji funkcji w czasie kompilacji.

Zauważ, że ze względu na instrukcję switch to, która funkcja zostanie faktycznie wywołana, jest określane dopiero w czasie wykonywania. Jest to jednak problem związany ze ścieżką wykonania, a nie z wiążącym problemem.

Późne wiązanie

W niektórych przypadkach wywołanie funkcji nie może zostać rozwiązane aż do czasu wykonania. W C++ jest to czasami znane jako późne wiązanie (lub w przypadku rozpoznawania funkcji wirtualnych, dynamiczne wysyłanie).

Nota autora

W ogólnej terminologii programowania termin „późne wiązanie” zwykle oznacza, że wywoływanej funkcji nie można określić na podstawie samych informacji o typie statycznym, ale należy ją rozwiązać przy użyciu informacji o typie dynamicznym.

W C++ termin ten jest częściej używany luźno oznacza dowolne wywołanie funkcji, w którym kompilator lub linker nie zna aktualnie wywoływanej funkcji.

W C++ jednym ze sposobów późnego wiązania jest użycie wskaźników funkcji. Aby w skrócie przejrzeć wskaźniki funkcji, wskaźnik funkcji jest typem wskaźnika, który wskazuje funkcję zamiast zmiennej. Funkcję, na którą wskazuje wskaźnik funkcji, można wywołać za pomocą operatora wywołania funkcji () na stronie wskaźnik.

Na przykład poniższy kod wywołuje funkcję printValue() przez wskaźnik funkcji:

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    auto fcn { printValue }; // create a function pointer and make it point to function printValue
    fcn(5);                  // invoke printValue indirectly through the function pointer

    return 0;
}

Wywołanie funkcji poprzez wskaźnik funkcji jest również znane jako pośrednie wywołanie funkcji. W momencie faktycznego wywołania fcn(5) kompilator nie wie w czasie kompilacji, jaka funkcja jest wywoływana. Zamiast tego w czasie wykonywania wykonywane jest pośrednie wywołanie funkcji do dowolnej funkcji znajdującej się pod adresem utrzymywany przez wskaźnik funkcji.

Dla zaawansowanych czytelników

Jeśli spojrzymy na kod asemblera wygenerowany dla wywołania fcn(5) (przy użyciu clang x86-64), zobaczymy coś takiego:

        lea     rax, [rip + printValue(int)] ; determine address of printValue and place into rax register
        mov     qword ptr [rbp - 8], rax     ; move value in rax register into memory associated with variable fcn

        mov     edi, 5                       ; copy argument 5 into edi register in preparation for function call
        call    qword ptr [rbp - 8]          ; invoke the function at the address held by variable fcn

Możesz wyraźnie zobaczyć, że jest to pośrednie wywołanie funkcji printValue(int) poprzez jej adres.

Następujący program kalkulatora jest funkcjonalnie identyczny z powyższym przykładem kalkulatora, z tą różnicą, że zamiast tego używa wskaźnika funkcji bezpośredniego wywołania funkcji:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int subtract(int x, int y)
{
    return x - y;
}

int multiply(int x, int y)
{
    return x * y;
}

int main()
{
    int x{};
    std::cout << "Enter a number: ";
    std::cin >> x;

    int y{};
    std::cout << "Enter another number: ";
    std::cin >> y;

    int op{};
    std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
    std::cin >> op;

    using FcnPtr = int (*)(int, int); // alias ugly function pointer type
    FcnPtr fcn { nullptr }; // create a function pointer object, set to nullptr initially

    // Set fcn to point to the function the user chose
    switch (op)
    {
        case 0: fcn = add; break;
        case 1: fcn = subtract; break;
        case 2: fcn = multiply; break;
        default:
            std::cout << "Invalid operator\n";
            return 1;
    }

    // Call the function that fcn is pointing to with x and y as parameters
    std::cout << "The answer is: " << fcn(x, y) << '\n';

    return 0;
}

W tym przykładzie zamiast bezpośrednio wywoływać funkcję add(), subtract() lub multiply() , zamiast tego ustawiliśmy fcn tak, aby wskazywał funkcję, którą chcemy wywołać. Następnie wywołujemy funkcję poprzez wskaźnik.

Kompilator nie może użyć wczesnego wiązania do rozwiązania wywołania funkcji fcn(x, y) , ponieważ nie jest w stanie określić, które wywołanie funkcji. funkcja fcn będzie wskazywała w czasie kompilacji!

Późne wiązanie jest nieco mniej wydajne, ponieważ wymaga dodatkowego poziomu pośredniego. W przypadku wczesnego wiązania procesor może przejść bezpośrednio do adresu funkcji. W przypadku późnego wiązania program musi odczytać adres przechowywany we wskaźniku, a następnie przejść do tego adresu. Wymaga to jednego dodatkowego kroku, co czyni go nieco wolniejszym. Jednakże zaletą późnego wiązania jest to, że jest ono bardziej elastyczne niż wczesne wiązanie, ponieważ decyduje o czym funkcja do wywołania nie musi być wykonywana przed wykonaniem.

W następnej lekcji przyjrzymy się, jak późne wiązanie jest wykorzystywane do implementacji funkcji wirtualnych.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
132 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze