Wyobraźmy sobie, że mamy strukturę do trzymania 3 podwójnych z niektórymi funkcjami składowymi:
struct Vector {
double x, y, z;
// ...
Vector &negate() {
x = -x; y = -y; z = -z;
return *this;
}
Vector &normalize() {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
// ...
};
Jest to trochę wymyślone dla uproszczenia, ale jestem pewien, że zgadzasz się, że istnieje podobny kod. Metody te pozwalają wygodnie łączyć, na przykład:
Vector v = ...;
v.normalize().negate();
Lub nawet:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Teraz, gdybyśmy zapewnili funkcje begin () i end (), moglibyśmy użyć naszego Vectora w nowej pętli for, powiedzmy aby wykonać pętlę po trzech współrzędnych x, y i z (bez wątpienia można skonstruować więcej "użytecznych" przykładów zastępując Vector np. String):
Vector v = ...;
for (double x : v) { ... }
Możemy nawet zrobić:
Vector v = ...;
for (double x : v.normalize().negate()) { ... }
i również:
for (double x : Vector{1., 2., 3.}) { ... }
Jednak następujący (wydaje mi się) jest uszkodzony:
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Chociaż wydaje się to logicznym połączeniem dwóch poprzednich zastosowań, myślę, że to ostatnie użycie tworzy zwisające odniesienie, podczas gdy poprzednie dwa są całkowicie w porządku.
- Czy jest to poprawne i powszechnie doceniane?
- Która część powyższego jest „złą” częścią, której należy unikać?
- Czy język zostałby ulepszony poprzez zmianę definicji pętli for opartej na zakresie, tak aby elementy tymczasowe skonstruowane w wyrażeniu for istniały przez cały czas trwania pętli?
Odpowiedzi:
Tak, twoje rozumienie rzeczy jest poprawne.
Zła część to pobranie odwołania do wartości l do tymczasowej wartości zwróconej z funkcji i powiązanie go z odwołaniem do wartości r. Jest tak źle, jak to:
auto &&t = Vector{1., 2., 3.}.normalize();
Okres
Vector{1., 2., 3.}
istnienia tymczasowego nie może zostać przedłużony, ponieważ kompilator nie ma pojęcia, że wartość zwracana znormalize
niego odwołuje się do niego.Byłoby to wysoce niezgodne z tym, jak działa C ++.
Czy zapobiegnie to pewnym pułapkom robionym przez ludzi używających wyrażeń łańcuchowych na obiektach tymczasowych lub różnych metod leniwej oceny wyrażeń? Tak. Ale wymagałoby to również specjalnego kodu kompilatora, a także byłoby mylące, dlaczego nie działa z innymi konstrukcjami wyrażeń.
O wiele bardziej rozsądnym rozwiązaniem byłoby poinformowanie kompilatora, że zwracana wartość funkcji jest zawsze odwołaniem
this
, a zatem jeśli wartość zwracana jest powiązana z konstrukcją rozszerzającą tymczasowo, wówczas rozszerzyłaby poprawną wartość tymczasową. Jest to jednak rozwiązanie na poziomie języka.Obecnie (jeśli kompilator to obsługuje), możesz sprawić, że
normalize
nie będzie można go wywołać tymczasowo:struct Vector { double x, y, z; // ... Vector &normalize() & { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } Vector &normalize() && = delete; };
Spowoduje to
Vector{1., 2., 3.}.normalize()
wyświetlenie błędu kompilacji, podczas gdyv.normalize()
będzie działać dobrze. Oczywiście nie będziesz w stanie zrobić poprawnych rzeczy, takich jak:Vector t = Vector{1., 2., 3.}.normalize();
Ale nie będziesz też w stanie robić niewłaściwych rzeczy.
Alternatywnie, jak sugerowano w komentarzach, możesz sprawić, by wersja referencyjna rvalue zwracała wartość zamiast referencji:
struct Vector { double x, y, z; // ... Vector &normalize() & { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } Vector normalize() && { Vector ret = *this; ret.normalize(); return ret; } };
Jeśli
Vector
był to typ z rzeczywistymi zasobami do przeniesienia, możesz użyćVector ret = std::move(*this);
zamiast tego. Nazwana optymalizacja wartości zwracanej sprawia, że jest to racjonalnie optymalne pod względem wydajności.źródło
delete
alternatywnej operacji, która zwraca wartość r:Vector normalize() && { normalize(); return std::move(*this); }
(Uważam, że wywołanienormalize
wewnątrz funkcji spowoduje wysłanie do przeciążenia lvalue, ale ktoś powinien to sprawdzić :)&
/&&
kwalifikacji metod. Czy pochodzi z C ++ 11, czy jest to jakieś (być może rozpowszechnione) zastrzeżone rozszerzenie kompilatora. Daje ciekawe możliwości.To nie jest ograniczenie języka, ale problem z twoim kodem. Wyrażenie
Vector{1., 2., 3.}
tworzy tymczasowy, alenormalize
funkcja zwraca odniesienie do l-wartości . Ponieważ wyrażenie jest lwartością , kompilator zakłada, że obiekt będzie żył, ale ponieważ jest to odniesienie do tymczasowego, obiekt umiera po ocenie pełnego wyrażenia, więc pozostaje wiszące odwołanie.Teraz, jeśli zmienisz projekt, aby zwracał nowy obiekt według wartości, a nie odwołanie do bieżącego obiektu, nie byłoby problemu, a kod działałby zgodnie z oczekiwaniami.
źródło
const
odniesienie wydłużyłoby żywotność obiektu w tym przypadku?normalize()
jako funkcji mutującej na istniejącym obiekcie. Stąd pytanie. To, że tymczasowy ma „wydłużoną żywotność”, gdy jest używany w konkretnym celu iteracji, a nie w innym przypadku, jest moim zdaniem mylącym błędem.const&
) ma wydłużony czas życia.Vector & r = Vector{1.,2.,3.}.normalize();
. Twój projekt ma to ograniczenie, a to oznacza, że albo chcesz zwrócić wartość (co może mieć sens w wielu okolicznościach, a zwłaszcza w przypadku odwołań do wartości r i przenieść ), albo musisz poradzić sobie z problemem w miejscu call: utwórz odpowiednią zmienną, a następnie użyj jej w pętli for. Zwróć również uwagę, że wyrażenieVector v = Vector{1., 2., 3.}.normalize().negate();
tworzy dwa obiekty ...T const& f(T const&);
jest całkowicie w porządku.T const& t = f(T());
jest całkowicie w porządku. A potem, w innej JT odkrywasz toT const& f(T const& t) { return t; }
i płaczesz… Jeślioperator+
operuje wartościami, to jest bezpieczniejsze ; wtedy kompilator może zoptymalizować kopiowanie (Want Speed? Pass by Values), ale to bonus. Jedynym powiązaniem tymczasowych, na które pozwoliłbym, jest powiązanie z odwołaniami do wartości r, ale funkcje powinny następnie zwracać wartości dla bezpieczeństwa i polegać na Copy Elision / Move Semantics.IMHO, drugi przykład jest już wadliwy. To, że zwracane przez modyfikujące operatory
*this
są wygodne w sposób, o którym wspomniałeś: pozwala na tworzenie łańcucha modyfikatorów. To może być wykorzystane do przekazania po prostu na skutek modyfikacji, ale robi to jest podatne na błędy, ponieważ może być łatwo przeoczyć. Jeśli zobaczę coś takiegoVector v{1., 2., 3.}; auto foo = somefunction1(v, 17); auto bar = somefunction2(true, v, 2, foo); auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));
Nie podejrzewałbym automatycznie, że funkcje modyfikują się
v
jako efekt uboczny. Oczywiście, że mogliby , ale byłoby to zagmatwane. Więc gdybym miał napisać coś takiego, upewniłbym się, żev
pozostanie to niezmienne. Na przykład dodałbym darmowe funkcjeauto normalized(Vector v) -> Vector {return v.normalize();} auto negated(Vector v) -> Vector {return v.negate();}
a następnie napisz pętle
for( double x : negated(normalized(v)) ) { ... }
i
for( double x : normalized(Vector{1., 2., 3}) ) { ... }
To jest lepiej czytelne IMO i jest bezpieczniejsze. Oczywiście wymaga to dodatkowej kopii, jednak w przypadku danych przydzielonych na stertę można by to prawdopodobnie zrobić tanią operacją przenoszenia w C ++ 11.
źródło