Tiefe des Kaninchenlochs oder C ++ - Interview bei PVS-Studio

Interview zu C ++ bei PVS-Studio

Autoren: Andrey Karpov, khandeliantsPhilip Handelyants.

Ich möchte eine interessante Situation mitteilen, als sich herausstellte, dass die Frage, die wir beim Interview verwendet haben, komplizierter war als vom Autor beabsichtigt. Mit der C ++ - Sprache und den Compilern sollten Sie immer auf der Hut sein. Langweile dich nicht.

Wie in jedem anderen Programmierunternehmen haben wir eine Reihe von Fragen für Interviews mit Stellenangeboten von Entwicklern in C ++, C # und Java. Wir haben viele Fragen mit doppeltem oder dreifachem Boden. Bei Fragen zu C # und Java können wir nicht sicher sagen, da sie andere Autoren haben. Viele der von Andrei Karpov für Interviews in C ++ zusammengestellten Fragen wurden jedoch sofort konzipiert, um die Tiefe des Wissens über die Sprachmerkmale zu untersuchen.

Diese Fragen können einfach richtig beantwortet werden. Sie können tiefer und tiefer gehen. Abhängig davon bestimmen wir während des Interviews, wie sehr eine Person mit den Nuancen der Sprache vertraut ist. Dies ist wichtig für uns, da wir einen Code-Analysator entwickeln und die Feinheiten und „Witze“ der Sprache sehr gut verstehen sollten.

Heute werden wir eine kurze Geschichte darüber erzählen, wie sich herausstellte, dass eine der ersten Fragen, die beim Interview gestellt wurden, noch tiefer ging als geplant. Also zeigen wir diesen Code:

void F1()
{
  int i = 1;
  printf("%d, %d\n", i++, i++);
}

und fragen: "Was wird gedruckt?"

Gute Frage. Kann sofort viel über Wissen sagen. Fälle, in denen eine Person ihm überhaupt nicht antworten kann, werden nicht berücksichtigt. Diese werden durch Vorversuche auf der HeadHunter-Website (hh.ru) beseitigt. Obwohl nein, es ist eine Lüge In unserer Erinnerung gab es einige einzigartige Persönlichkeiten, die etwas im Geiste beantworteten:

Dieser Code druckt zu Beginn einen Prozentsatz, dann d, dann einen weiteren Prozentsatz, d, dann einen Zauberstab, n und dann zwei Einheiten.

In solchen Fällen endet das Interview natürlich schnell.

Also jetzt zurück zum normalen Interview :). Oft antworten sie so:

1 und 2 werden gedruckt.

Dies ist die Antwort des Praktikantenlevels. Ja, solche Werte können natürlich gedruckt werden, aber wir warten auf ungefähr die folgende Antwort:

Dies bedeutet nicht, was genau dieser Code drucken wird. Dies ist ein nicht spezifiziertes (oder undefiniertes) Verhalten. Die Reihenfolge, in der die Argumente berechnet werden, ist nicht definiert. Alle Argumente müssen ausgewertet werden, bevor der Hauptteil der aufgerufenen Funktion ausgeführt wird. Die Reihenfolge, in der dies geschieht, liegt jedoch im Ermessen des Compilers. Daher kann der Code sehr gut sowohl "1, 2" als auch umgekehrt "2, 1" drucken. Im Allgemeinen ist das Schreiben eines solchen Codes höchst unerwünscht. Wenn er von mindestens zwei Compilern erstellt wird, ist es möglich, „in den Fuß zu schießen“. Und viele Compiler werden hier eine Warnung geben.

In der Tat, wenn Sie Clang verwenden , können Sie "1, 2" erhalten.

Und wenn Sie GCC verwenden , können Sie "2, 1" erhalten.

Es war einmal ein Versuch mit dem MSVC-Compiler, der auch "2, 1" produzierte. Keine Anzeichen von Ärger.

Vor kurzem war es für allgemeine Zwecke von Drittanbietern erneut erforderlich, diesen Code mit modernem Visual C ++ zu kompilieren und auszuführen. Zusammengestellt unter Release-Konfiguration mit aktivierter / O2- Optimierung . Und wie sie sagen, haben sie Abenteuer auf eigene Faust gefunden :). Was denken Sie, ist passiert? Ha! Hier ist was: "1, 1".

Überlegen Sie also, was Sie wollen. Es stellt sich heraus, dass die Frage noch tiefer und verwirrender ist. Wir selbst haben nicht damit gerechnet.

Da der C ++ - Standard die Berechnung von Argumenten in keiner Weise regelt, interpretiert der Compiler diese Art von nicht spezifiziertem Verhalten auf eine sehr eigenartige Weise. Werfen wir einen Blick auf den Assembler-Code, der vom MSVC 19.25-Compiler (Microsoft Visual Studio Community 2019, Version 16.5.1) generiert wurde. Die Flag-Version des Sprachstandards lautet '/ std: c ++ 14':


Formal hat der Optimierer den obigen Code wie folgt umgewandelt:

void F1()
{
  int i = 1;
  int tmp = i;
  i += 2;
  printf("%d, %d\n", tmp, tmp);
}

Aus Sicht des Compilers ändert eine solche Optimierung nichts am beobachteten Verhalten des Programms. Wenn Sie sich das ansehen , werden Sie verstehen, dass der C ++ 11-Standard aus gutem Grund neben intelligenten Zeigern auch die „magische“ Funktion make_shared hinzugefügt hat (und C ++ 14 auch make_unique hinzugefügt hat ). Solch ein harmloses Beispiel und kann auch "Brennholz brechen":

void foo(std::unique_ptr<int>, std::unique_ptr<double>);

int main()
{
  foo(std::unique_ptr<int> { new int { 0 } },
      std::unique_ptr<double> { new double { 0.0 } });
}

Ein gerissener Compiler kann dies in die folgende Reihenfolge von Berechnungen umwandeln (zum Beispiel dieselbe MSVC ):

new int { .... };
new double { .... };
std::unique_ptr<int>::unique_ptr
std::unique_ptr<double>::unique_ptr

Wenn der zweite Aufruf des neuen Operators eine Ausnahme auslöst, tritt ein Speicherverlust auf.

Aber zurück zum ursprünglichen Thema. Trotz der Tatsache, dass aus Sicht des Compilers alles in Ordnung war, waren wir uns immer noch sicher, dass die Ausgabe „1, 1“ falsch wäre, um das vom Entwickler erwartete Verhalten zu berücksichtigen. Und dann haben wir versucht, den Quellcode mit dem MSVC-Compiler mit dem Standardversionsflag '/ std: c ++ 17' zu kompilieren. Und alles beginnt wie erwartet zu funktionieren und "2, 1" wird gedruckt. Schauen Sie sich den Assembler-Code an:


Alles ist fair, der Compiler hat die Werte 2 und 1 als Argumente übergeben. Aber warum hat sich alles so dramatisch verändert? Es stellt sich heraus, dass dem C ++ 17-Standard Folgendes hinzugefügt wurde:

Der Postfix-Ausdruck wird vor jedem Ausdruck in der Ausdrucksliste und vor jedem Standardargument sequenziert. Die Initialisierung eines Parameters, einschließlich aller zugehörigen Wertberechnungen und Nebenwirkungen, wird in Bezug auf die eines anderen Parameters unbestimmt sequenziert.

Der Compiler hat weiterhin das Recht, Argumente in einer beliebigen Reihenfolge zu berechnen. Ab dem C ++ 17-Standard hat er nun das Recht, das nächste Argument und seine Nebenwirkungen erst ab dem Zeitpunkt zu berechnen, an dem alle Berechnungen und Nebenwirkungen des vorherigen Arguments abgeschlossen sind.

Übrigens, wenn Sie dasselbe Beispiel mit intelligenten Zeigern mit dem Flag '/ std: c ++ 17' kompilieren, wird dort alles in Ordnung - die Verwendung von std :: make_unique ist jetzt optional.

Hier ist ein weiteres Maß für die Tiefe in der Frage, die sich herausstellte. Es gibt eine Theorie, aber es gibt Praxis in Form eines bestimmten Compilers oder einer anderen Interpretation des Standards :). Die Welt von C ++ ist immer komplexer und unerwarteter als es scheint.

Wenn jemand genauer erklären kann, was passiert, teilen Sie uns dies bitte in den Kommentaren mit. Wir müssen die Frage endlich verstehen, um zumindest selbst die Antwort im Interview zu erfahren! :) :)

Hier ist so eine informative Geschichte. Wir hoffen, es war interessant und Sie teilen Ihre Meinung zu diesem Thema. Wir empfehlen, so weit wie möglich den modernsten Sprachstandard zu verwenden, um weniger überrascht zu sein, dass dies derzeit optimierende Compiler können. Besser noch, schreibe diesen Code überhaupt nicht :).

PS Wir können sagen, dass wir die Frage „beleuchtet“ haben und sie nun aus dem Fragebogen entfernt werden muss. Wir sehen den Punkt darin nicht. Wenn eine Person nicht zu faul ist, unsere Veröffentlichungen vor einem Interview zu studieren, dieses Material liest und es dann verwendet, dann ist sie gut gemacht und erhält zu Recht ein Pluszeichen :).


Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Andrey Karpov, Phillip Khandeliants. Wie tief das Kaninchenloch geht, oder C ++ - Vorstellungsgespräche bei PVS-Studio .

All Articles