Der folgende Code sieht auf den ersten Blick ziemlich harmlos aus. Ein Benutzer verwendet die Funktion, bar()
um mit einigen Bibliotheksfunktionen zu interagieren. (Möglicherweise hat dies sogar schon lange funktioniert, nachdem bar()
ein Verweis auf einen nicht temporären Wert oder Ähnliches zurückgegeben wurde.) Jetzt wird jedoch einfach eine neue Instanz von zurückgegeben B
. B
hat wieder eine Funktion a()
, die einen Verweis auf ein Objekt des iterierbaren Typs zurückgibt A
. Der Benutzer möchte dieses Objekt abfragen, was zu einem Segfault führt, da das von zurückgegebene temporäre B
Objekt bar()
zerstört wird, bevor die Iteration beginnt.
Ich bin mir nicht sicher, wer (Bibliothek oder Benutzer) dafür verantwortlich ist. Alle von der Bibliothek bereitgestellten Klassen sehen für mich sauber aus und machen auf keinen Fall etwas anderes (Rückgabe von Verweisen auf Mitglieder, Rückgabe von Stack-Instanzen, ...) als so viel anderer Code da draußen. Der Benutzer scheint auch nichts falsch zu machen, er iteriert nur über ein Objekt, ohne irgendetwas in Bezug auf die Lebensdauer dieses Objekts zu tun.
(Eine verwandte Frage könnte sein: Sollte man die allgemeine Regel festlegen, dass Code nicht "bereichsbasiert für Iteration" über etwas sein sollte, das von mehr als einem verketteten Aufruf im Loop-Header abgerufen wird, da einer dieser Aufrufe u. U. a. Zurückgibt rWert?)
#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;
}
}
Antworten:
Ich denke, das grundlegende Problem ist eine Kombination von Sprachfunktionen (oder deren Fehlen) von C ++. Sowohl der Bibliothekscode als auch der Clientcode sind vernünftig (was durch die Tatsache belegt wird, dass das Problem keineswegs offensichtlich ist). Wenn die Lebensdauer des Provisoriums
B
angemessen verlängert würde (bis zum Ende der Schleife), gäbe es kein Problem.Es ist extrem schwer, das temporäre Leben gerade lang genug und nicht länger zu machen. Nicht einmal ein eher ad-hoc "alle an der Erstellung des Sortiments für ein Live-basiertes Sortiment bis zum Ende der Schleife beteiligten Provisorien" wären ohne Nebenwirkungen. Betrachten Sie den Fall der
B::a()
Rückgabe eines Bereichs, derB
vom Wert des Objekts unabhängig ist. Dann kann das ProvisoriumB
sofort verworfen werden. Selbst wenn man genau die Fälle identifizieren könnte, in denen eine Verlängerung der Lebensdauer erforderlich ist, da diese Fälle für Programmierer nicht offensichtlich sind, wäre der Effekt (Destruktoren werden viel später aufgerufen) überraschend und möglicherweise eine ebenso subtile Fehlerquelle.Es wäre wünschenswerter, nur solchen Unsinn zu entdecken und zu verbieten, und den Programmierer zu zwingen, explizit
bar()
zu einer lokalen Variablen zu wechseln . Dies ist in C ++ 11 nicht möglich und wird wahrscheinlich auch nicht möglich sein, da Anmerkungen erforderlich sind. Rust macht das, wo die Signatur von.a()
wäre:Hierbei
'x
handelt es sich um eine lebenslange Variable oder Region, bei der es sich um einen symbolischen Namen für den Zeitraum handelt, in dem eine Ressource verfügbar ist. Ehrlich gesagt sind die Lebenszeiten schwer zu erklären - oder wir haben noch nicht die beste Erklärung gefunden -, also beschränke ich mich auf das für dieses Beispiel erforderliche Minimum und verweise den geneigten Leser auf die offizielle Dokumentation .Der Leihprüfer würde feststellen, dass das Ergebnis von
bar().a()
so lange leben muss, wie die Schleife läuft. Phrasiert als Einschränkung auf die Lebensdauer'x
, schreiben wir:'loop <= 'x
. Es wird auch bemerkt, dass der Empfänger des Methodenaufrufsbar()
ein temporärer ist. Die beiden Zeiger sind mit derselben Lebensdauer verknüpft, daher'x <= 'temp
gibt es eine weitere Einschränkung.Diese beiden Einschränkungen sind widersprüchlich! Wir brauchen
'loop <= 'x <= 'temp
aber'temp <= 'loop
, was das Problem ziemlich genau erfasst. Aufgrund der widersprüchlichen Anforderungen wird der Fehlercode abgelehnt. Beachten Sie, dass dies eine Überprüfung zur Kompilierungszeit ist und Rust-Code in der Regel denselben Computercode ergibt wie äquivalenter C ++ - Code, sodass Sie keine Laufzeitkosten dafür bezahlen müssen.Trotzdem ist dies eine wichtige Funktion, die einer Sprache hinzugefügt werden kann. Sie funktioniert nur, wenn der gesamte Code sie verwendet. Das Design von APIs ist ebenfalls betroffen (einige Designs, die in C ++ zu gefährlich wären, werden praktisch, andere können nicht dazu gebracht werden, mit den Lebensdauern zufrieden zu sein). Leider bedeutet dies, dass es nicht praktisch ist, C ++ (oder eine andere Sprache) rückwirkend hinzuzufügen. Zusammenfassend liegt der Fehler in der Trägheit, die erfolgreiche Sprachen haben, und in der Tatsache, dass Bjarne 1983 nicht über die Kristallkugel und die Weitsicht verfügte, um die Lehren aus den letzten 30 Jahren der Forschung und C ++ - Erfahrung einzubeziehen ;-)
Dies ist natürlich keineswegs hilfreich, um das Problem in Zukunft zu vermeiden (es sei denn, Sie wechseln zu Rust und verwenden C ++ nie wieder). Man könnte längere Ausdrücke mit mehreren verketteten Methodenaufrufen vermeiden (was ziemlich einschränkend ist und nicht einmal alle Probleme auf Lebenszeit remote behebt). Oder man könnte versuchen , eine disziplinierte Eigentumspolitik ohne Compiler Unterstützung Annahme: Dokument deutlich , dass
bar
Renditen von Wert und das ErgebnisB::a()
muss überleben nicht dieB
auf dema()
aufgerufen wird. Beachten Sie, dass es sich um eine Vertragsänderung handelt, wenn Sie eine Funktion so ändern, dass sie nach Wert zurückgegeben wird, anstatt nach einer längerfristigen Referenz . Es ist immer noch fehleranfällig, kann jedoch die Ermittlung der Ursache beschleunigen.quelle
Können wir dieses Problem mithilfe von C ++ - Funktionen lösen?
C ++ 11 hat Member Function Ref-Qualifiers hinzugefügt, mit denen die Wertkategorie der Klasseninstanz (Ausdruck) eingeschränkt werden kann, auf die die Member Function aufgerufen werden kann. Beispielsweise:
Wenn
begin
wir die Member-Funktion aufrufen , wissen wir, dass wir höchstwahrscheinlich auch dieend
Member-Funktion aufrufen müssen (oder so ähnlichsize
, um die Größe des Bereichs zu ermitteln). Dies erfordert, dass wir mit einem Wert arbeiten, da wir ihn zweimal ansprechen müssen. Sie können daher argumentieren, dass diese Mitgliedsfunktionen lvalue-ref-qualifiziert sein sollten.Dies könnte jedoch das zugrunde liegende Problem nicht lösen: Aliasing. Die Funktion
begin
und derend
Member bilden einen Alias für das Objekt oder die vom Objekt verwalteten Ressourcen. Wenn wirbegin
undend
durch eine einzelne Funktion ersetzenrange
, sollten wir eine bereitstellen, die auf rvalues aufgerufen werden kann:Dies mag ein gültiger Anwendungsfall sein, aber die obige Definition von lässt dies
range
nicht zu. Da wir das temporäre Objekt nach dem Aufruf der Member-Funktion nicht ansprechen können, ist es möglicherweise sinnvoller, einen Container, dh einen Besitzerbereich, zurückzugeben:Anwendung auf den Fall des OP und leichte Codeüberprüfung
Diese Mitgliedsfunktion ändert die Wertkategorie des Ausdrucks:
B()
ist ein Wert, aberB().a()
ein Wert. Auf der anderen SeiteB().m_a
ist ein Wert. Beginnen wir also damit, dies konsistent zu machen. Hierfür gibt es zwei Möglichkeiten:Die zweite Version behebt, wie oben erwähnt, das Problem im OP.
Zusätzlich können wir die
B
Mitgliedsfunktionen einschränken :Dies hat keine Auswirkungen auf den OP-Code, da das Ergebnis des Ausdrucks nach der
:
in der bereichsbasierten for-Schleife an eine Referenzvariable gebunden ist. Und diese Variable (als Ausdruck für den Zugriff auf ihrebegin
undend
Member-Funktionen) ist ein lWert.Die Frage ist natürlich, ob die Standardregel lautet "Aliasing von Member-Funktionen auf R-Werte sollte ein Objekt zurückgeben, das alle seine Ressourcen besitzt, es sei denn, es gibt einen guten Grund, dies nicht zu tun" . Der zurückgegebene Alias kann legal verwendet werden, ist jedoch in der Art und Weise, in der Sie ihn erleben, gefährlich: Er kann nicht verwendet werden, um die Lebensdauer seines "übergeordneten" temporären Objekts zu verlängern:
In C ++ 2a sollten Sie dieses (oder ein ähnliches) Problem folgendermaßen umgehen:
anstelle der OP's
Die Problemumgehung gibt manuell an, dass die Lebensdauer von
b
den gesamten Block der For-Schleife ist.Vorschlag, der diese Init-Anweisung einleitete
Live-Demo
quelle