Pytanie

Jaki output będzie miała poniższa funkcja foo:

struct S
{
    int val{};
};

auto foo(S & s1, S & s2)
{
    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

Rozwiązanie

Na początku zaznaczę tylko, że będziemy korzystać ze standardu C++14, ponieważ w powyższej funkcji foo, dzięki słowu kluczowemu auto, mamy dedukcję typu zwracanego (w rzeczywistości nie ma to jednak większego znaczenia z punktu widzenia tego zadania – funkcja foo zwraca po prostu wartość typu int).

Przejdźmy teraz do naszego pytania. Jest ono nieco podchwytliwe, bowiem wynik zależy od argumentów przekazanych do funkcji foo (parametry s1s2), pomimo tego, że są one tam nadpisywane (inicjalizacja składowej val wewnątrz struktury S nie ma tu znaczenia).

Zacznijmy od bardziej oczywistego przypadku, kiedy to wartość zwracana wynosi 49 :

#include <iostream>

struct S
{
    int val{};
};

auto foo(S & s1, S & s2)
{
    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

int main()
{
    S s1;
    S s2;

    // Case 1
    std::cout << foo(s1, s2) << std::endl;
}

Uruchom w edytorze

49

Jak widać wynik jest zgodny z oczekiwaniami, bowiem do funkcji foo zostały przekazane dwa różne obiekty typu S.

Rozważmy jeszcze jeden możliwy przypadek, kiedy to do funkcji foo dwukrotnie przekazujemy ten sam obiekt typu S:

#include <iostream>

struct S
{
    int val{};
};

auto foo(S & s1, S & s2)
{
    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

int main()
{
    S s1;
    S s2;

    // Case 1
    std::cout << foo(s1, s2) << std::endl;

    // Case 2
    std::cout << foo(s1, s1) << std::endl;
}

Uruchom w edytorze

49
14

Tym razem otrzymaliśmy inny wynik, bowiem w funkcji foo nadpisujemy dwukrotnie tę samą składową val. Za drugim razem jest to wartość 7 i po dodaniu jej do siebie samej otrzymamy w rezultacie wartość 14. W tym miejscu warto zaznaczyć, że język C++ nie zabrania tego typu aliasingu,  jeżeli przede wszystkim typy parametrów są takie same (w przeciwnym przypadku należy liczyć się z niezdefiniowanym zachowaniem – wynika to z działania określanego jako type punning, którego omówienie wymagałoby osobnego wpisu).

Na zakończenie zastanówmy się jak rozwiązać problem ewentualnego, niechcianego aliasingu. W tym celu posłużymy się naszym przykładem. Oczywistym i najprostszym rozwiązaniem jest tutaj zwyczajne porównywanie adresów obiektów (wskaźników) przekazanych do funkcji foo:

#include <iostream>
#include <cassert>

struct S
{
    int val{};
};

auto foo(S & s1, S & s2)
{
    assert(&s1 != &s2
           && "s1 and s2 are the same object");

    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

int main()
{
    S s1;
    S s2;

    // Case 1
    std::cout << foo(s1, s2) << std::endl;

    // Case 2
    std::cout << foo(s1, s1) << std::endl;
}

Uruchom w edytorze

49
output.s: ./example.cpp:11: auto foo(S&, S&): Assertion `&s1 != &s2 && "s1 and s2 are the same object"' failed.

Jak widać w przypadku przekazania tego samego obiektu do funkcji foo program kończony jest w ramach asercjimakro assert (plik nagłowkowy cassert).

W tym miejscu moglibyśmy zakończyć ten artykuł. Ja jednak chciałbym zwrócić uwagę na jeszcze jedno zagadnienie. Podczas pobierania adresu obiektu używamy oczywiście operatora &. Operator ten, możemy jednak zdefiniować samodzielnie:

#include <iostream>
#include <cassert>

struct S
{
    // operator & defined by the user
    S *operator &()
    {
        return this;
    }

    int val{};
};

auto foo(S & s1, S & s2)
{
    assert(&s1 != &s2
           && "s1 and s2 are the same object");

    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

int main()
{
    S s1;
    S s2;

    // Case 1
    std::cout << foo(s1, s2) << std::endl;

    // Case 2
    std::cout << foo(s1, s1) << std::endl;
}

Uruchom w edytorze

49
output.s: ./example.cpp:17: auto foo(S&, S&): Assertion `&s1 != &s2 && "s1 and s2 are the same object"' failed.

Nasz program zadziałał tak samo jak w przypadku domyślnego operatora &. Nic jednak nie stoi na przeszkodzie aby ten sam  operator zwracał inna wartość niż wskażnik this. Mało tego, jego typ zwracany może być inny niż wskaźnik do obiektu dla klasy w której się znajduje (w naszym przypadku będzie to typ int * odpowiadający wskaźnikowi do składowej val):

#include <iostream>
#include <cassert>

struct S
{
    // operator & defined by the user
    int *operator &()
    {
        return val ? &val : nullptr;
    }

    int val{};
};

auto foo(S & s1, S & s2)
{
    assert(&s1 != &s2
           && "s1 and s2 are the same object");

    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

int main()
{
    S s1;
    S s2;

    // Case 1
    std::cout << foo(s1, s2) << std::endl;

    // Case 2
    std::cout << foo(s1, s1) << std::endl;
}

Uruchom w edytorze

output.s: ./example.cpp:17: auto foo(S&, S&): Assertion `&s1 != &s2 && "s1 and s2 are the same object"' failed.

Okazuje się, że tym razem program kończy się w ramach asercji już w pierwszym przypadku, czyli gdy do funkcji foo przekazano różne obiekty typu S (operator & zwraca taką samą wartość nullpltr dla obu obiektów – ze względu na wartość składowej val równą 0).

Czy można więc pobrać adres obiektu pomimo zdefiniowania operatora & ? Wraz z pojawieniem się standardu C++11 otrzymaliśmy odpowiednie narzędzie w postaci funkcji addressof (plik nagłówkowy memory):

#include <iostream>
#include <memory>
#include <cassert>

struct S
{
    // operator & defined by the user
    int *operator &()
    {
        return val ? &val : nullptr;
    }

    int val{};
};

auto foo(S & s1, S & s2)
{
    assert(std::addressof(s1) != std::addressof(s2)
           && "s1 and s2 are the same object");

    s1.val = 42;
    s2.val = 7;

    return s1.val + s2.val; 
}

int main()
{
    S s1;
    S s2;

    // Case 1
    std::cout << foo(s1, s2) << std::endl;

    // Case 2
    std::cout << foo(s1, s1) << std::endl;
}

Uruchom w edytorze

49
output.s: ./example.cpp:18: auto foo(S&, S&): Assertion `&s1 != &s2 && "s1 and s2 are the same object"' failed.

Podsumowując, domyślny operator & okaże się wystarczający w większości przypadków. Jednakże jeśli twoja klasa definiuje operator & lub pracujesz z kodem generycznym opartym na szablonach (nie możesz więc z góry założyć, że otrzymywane typy nie implementują operatora &), wówczas warto rozważyć zastosowanie funkcji addressof.