Kto ponosi winę za ten zakres w oparciu o odniesienie do tymczasowości?


15

Poniższy kod wygląda na pierwszy rzut oka raczej nieszkodliwie. Użytkownik używa tej funkcji bar()do interakcji z niektórymi funkcjami biblioteki. (Mogło to nawet działać przez długi czas, odkąd bar()zwróciło odwołanie do wartości nietrwałej lub podobnej.) Teraz jednak zwraca po prostu nową instancję B. Bponownie ma funkcję, a()która zwraca odwołanie do obiektu typu iterowalnego A. Użytkownik chce wysłać zapytanie do tego obiektu, co prowadzi do uszkodzenia pliku, ponieważ Bzwrócony obiekt tymczasowy bar()jest niszczony przed rozpoczęciem iteracji.

Jestem niezdecydowany, kto (biblioteka lub użytkownik) jest winien za to. Wszystkie klasy dostarczone mi z biblioteki wyglądają dla mnie na czyste i na pewno nie robią nic innego (zwracanie referencji do członków, zwracanie instancji stosu, ...) niż wiele innych kodów. Wydaje się, że użytkownik nie robi nic złego, po prostu iteruje jakiś obiekt, nie robiąc nic w odniesieniu do jego żywotności.

(Powiązane pytanie może brzmieć: jeśli należy ustanowić ogólną zasadę, że kod nie powinien „opierać się na zakresie iteracji” nad czymś, co jest pobierane przez więcej niż jedno połączenie łańcuchowe w nagłówku pętli, ponieważ każde z tych wywołań może zwrócić wartość?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
Kiedy dowiesz się, kogo winić, jaki będzie następny krok? Krzyczy na niego / nią?
JensG

7
Nie, dlaczego miałbym? W rzeczywistości bardziej interesuje mnie, gdzie proces myślenia nad opracowaniem tego „programu” nie pozwolił uniknąć tego problemu w przyszłości.
hllnll

Nie ma to nic wspólnego z wartościami wartości lub pętlami opartymi na zakresie, ale z tym, że użytkownik nie rozumie właściwie życia obiektu.
James

Uwaga strony: To jest CWG 900, który został zamknięty jako brak usterki. Może minuty zawierają trochę dyskusji.
dyp

8
Kto jest za to winien? Bjarne Stroustrup i Dennis Ritchie, przede wszystkim.
Mason Wheeler,

Odpowiedzi:


14

Myślę, że podstawowym problemem jest połączenie cech językowych (lub ich brak) w C ++. Zarówno kod biblioteki, jak i kod klienta są rozsądne (o czym świadczy fakt, że problem nie jest oczywisty). Gdyby żywotność tymczasowa Bbyła odpowiednio przedłużona (do końca pętli), nie byłoby problemu.

Sprawienie, by życie tymczasowe było wystarczająco długie i już nie jest niezwykle trudne. Nawet ad-hoc „wszystkie tymczasowe działania związane z tworzeniem zasięgu na podstawie zasięgu na żywo do końca pętli” nie byłyby pozbawione skutków ubocznych. Rozważ przypadek B::a()zwrócenia zakresu niezależnego od Bobiektu według wartości. Następnie tymczasowe Bmożna natychmiast odrzucić. Nawet gdyby można było precyzyjnie zidentyfikować przypadki, w których konieczne jest przedłużenie życia, ponieważ przypadki te nie są oczywiste dla programistów, efekt (niszczyciele nazywane znacznie później) byłby zaskakujący i być może równie subtelnym źródłem błędów.

Bardziej pożądane byłoby po prostu wykrycie i zakazanie takich bzdur, zmuszając programistę do wyraźnego podniesienia poziomu bar()do zmiennej lokalnej. Nie jest to możliwe w C ++ 11 i prawdopodobnie nigdy nie będzie możliwe, ponieważ wymaga adnotacji. Rdza robi to, gdzie podpisem .a()byłoby:

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

Tutaj 'x zmienna lub region na całe życie, która jest symboliczną nazwą okresu, przez który zasób jest dostępny. Szczerze mówiąc, wcielenia są trudne do wyjaśnienia - lub jeszcze nie wymyśliliśmy najlepszego wyjaśnienia - więc ograniczę się do minimum niezbędnego dla tego przykładu i skieruję skłonnego czytelnika do oficjalnej dokumentacji .

Narzędzie sprawdzania pożyczek zauważyłoby, że wynik bar().a()potrzeby musi istnieć tak długo, jak długo działa pętla. Sformułowane jako ograniczenie na całe życie 'x, możemy napisać: 'loop <= 'x. Zauważyłby również, że odbiorca wywołania metody bar(), jest tymczasowy. Te dwa wskaźniki są powiązane z tym samym czasem życia, dlatego 'x <= 'tempjest kolejnym ograniczeniem.

Te dwa ograniczenia są ze sobą sprzeczne! Potrzebujemy, 'loop <= 'x <= 'tempale 'temp <= 'loop, co dość dokładnie ujmuje problem. Ze względu na sprzeczne wymagania błędny kod jest odrzucany. Zauważ, że jest to kontrola czasu kompilacji, a kod Rust zwykle daje ten sam kod maszynowy co równoważny kod C ++, więc nie musisz ponosić za to kosztów wykonawczych.

Niemniej jednak jest to duża funkcja, którą można dodać do języka i działa tylko wtedy, gdy korzysta z niej cały kod. wpływa to również na projektowanie interfejsów API (niektóre projekty, które byłyby zbyt niebezpieczne w C ++ stają się praktyczne, inne nie mogą sprawić, aby grały się przyjemnie przez całe życie). Niestety, oznacza to, że nie jest praktyczne dodawanie do C ++ (lub jakiegokolwiek innego języka naprawdę) z mocą wsteczną. Podsumowując, wina leży w tym, że języki, które odnosiły sukcesy w zakresie bezwładności, oraz fakt, że Bjarne w 1983 r. Nie miał kryształowej kuli i nie przewidywałby włączenia lekcji ostatnich 30 lat badań i doświadczenia w C ++ ;-)

Oczywiście nie jest to wcale pomocne w uniknięciu problemu w przyszłości (chyba że przejdziesz na Rust i nigdy więcej nie użyjesz C ++). Można uniknąć dłuższych wyrażeń za pomocą wielu łańcuchowych wywołań metod (co jest dość ograniczające, a nawet nie naprawia wszystkich problemów przez całe życie). Lub można spróbować zastosować bardziej zdyscyplinowaną politykę własności bez pomocy kompilatora: Dokument wyraźnie barzwraca wartość i że wynik B::a()nie może przeżyć tego, Bna który a()się powołuje. Zmieniając funkcję, aby zwracała wartość zamiast odwołania o dłuższym okresie życia, należy pamiętać, że jest to zmiana kontraktu . Nadal jest podatny na błędy, ale może przyspieszyć proces identyfikacji przyczyny, kiedy to nastąpi.


14

Czy możemy rozwiązać ten problem za pomocą funkcji C ++?

W C ++ 11 dodano kwalifikatory ref funkcji członka, które pozwalają ograniczyć kategorię wartości instancji klasy (wyrażenia), do której można wywołać funkcję członka. Na przykład:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

Kiedy wywołujemy funkcję beginczłonka, wiemy, że najprawdopodobniej będziemy musieli również wywołać funkcję endczłonka (lub coś w tym rodzaju size, aby uzyskać rozmiar zakresu). Wymaga to działania na podstawie wartości, ponieważ musimy rozwiązać ją dwukrotnie. W związku z tym można argumentować, że te funkcje składowe powinny być kwalifikowane do wartości referencyjnej.

Może to jednak nie rozwiązać podstawowego problemu: aliasing. Funkcja begini endczłonek alias obiektu lub zasobów zarządzanych przez obiekt. Jeśli zastąpimy begina endprzez jedną funkcję range, powinniśmy zapewnić taki, który można nazwać na rvalues:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

Może to być poprawny przypadek użycia, ale powyższa definicja rangego nie zezwala. Ponieważ nie możemy rozwiązać problemu tymczasowego po wywołaniu funkcji składowej, bardziej uzasadnione może być zwrócenie kontenera, tzn. Zakresu własności:

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

Zastosowanie tego do przypadku PO i drobna weryfikacja kodu

struct B {
    A m_a;
    A & a() { return m_a; }
};

Ta funkcja członka zmienia kategorię wartości wyrażenia: B()jest wartością, ale B().a()jest wartością. Z drugiej strony B().m_ajest warta uwagi. Zacznijmy od uczynienia tego spójnym. Można to zrobić na dwa sposoby:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

Druga wersja, jak wspomniano powyżej, naprawi problem w PO.

Dodatkowo możemy ograniczyć Bfunkcje członka:

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

Nie będzie to miało żadnego wpływu na kod OP, ponieważ wynik wyrażenia po :pętli for opartej na zakresie jest powiązany ze zmienną referencyjną. Ta zmienna (jako wyrażenie używane w celu uzyskania dostępu do jej funkcji begini elementów endczłonkowskich) jest wartością.

Oczywiście pytanie brzmi, czy domyślną regułą powinno być „aliasing funkcji składowych na wartościach rv powinien zwrócić obiekt, który jest właścicielem wszystkich jego zasobów, chyba że istnieje dobry powód, aby tego nie robić” . Zwracany alias może być legalnie używany, ale jest niebezpieczny w sposobie, w jaki go doświadczasz: nie można go użyć do przedłużenia żywotności jego „rodzica” tymczasowego:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

W C ++ 2a myślę, że powinieneś obejść ten (lub podobny) problem w następujący sposób:

for( B b = bar(); auto i : b.a() )

zamiast PO

for( auto i : bar().a() )

Obejście to ręcznie określa, że ​​czas życia bto cały blok pętli for.

Propozycja, która wprowadziła to oświadczenie init

Demo na żywo


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.