Profondeur du lapin ou interview C ++ chez PVS-Studio

Interview sur C ++ chez PVS-Studio

Auteurs: Andrey Karpov, khandeliantsPhilip Handelyants.

Je voudrais partager une situation intéressante lorsque la question que nous avons utilisée lors de l'entretien s'est avérée plus compliquée que ne le voulait l'auteur. Avec le langage C ++ et les compilateurs, vous devez toujours être à l'affût. Ne vous ennuyez pas.

Comme dans toute autre entreprise de programmation, nous avons des séries de questions à interviewer pour les postes vacants de développeurs en C ++, C # et Java. Nous avons de nombreuses questions à double ou triple fond. Pour les questions sur C # et Java, nous ne pouvons pas dire avec certitude, car ils ont d'autres auteurs. Mais bon nombre des questions compilées par Andrei Karpov pour des interviews en C ++ ont été immédiatement conçues pour sonder la profondeur de la connaissance des fonctionnalités du langage.

Ces questions peuvent recevoir une réponse simple et correcte. Vous pouvez aller de plus en plus profondément. En fonction de cela, au cours de l'entretien, nous déterminons dans quelle mesure une personne connaît les nuances de la langue. C'est important pour nous, car nous développons un analyseur de code et devons très bien comprendre les subtilités du langage et les «blagues».

Aujourd'hui, nous allons raconter une courte histoire sur la façon dont l'une des premières questions posées lors de l'entretien s'est avérée encore plus profonde que ce que nous avions prévu. Donc, nous montrons ce code:

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

et demandez: "Qu'est-ce qui sera imprimé?"

Bonne question. Peut immédiatement en dire long sur la connaissance. Les cas où une personne ne peut pas du tout lui répondre ne seront pas pris en compte. Ceux-ci sont éliminés par des tests préliminaires sur le site Web HeadHunter (hh.ru). Mais non, ce sont des conneries. Dans notre mémoire, il y avait quelques personnalités uniques qui ont répondu quelque chose dans l'esprit:

Ce code imprimera au début un pourcentage, puis d, puis un autre pourcentage, d, puis une baguette, n, puis deux unités.

De toute évidence, dans de tels cas, l'entretien se termine rapidement.

Revenons donc à l'entretien normal :). Souvent, ils répondent comme ceci:

1 et 2 s'impriment

C'est la réponse du niveau interne. Oui, de telles valeurs peuvent bien sûr être imprimées, mais nous attendons approximativement la réponse suivante:

Cela ne veut pas dire exactement ce que ce code va imprimer. Il s'agit d'un comportement non spécifié (ou non défini). L'ordre dans lequel les arguments sont calculés n'est pas défini. Tous les arguments doivent être évalués avant l'exécution du corps de la fonction appelée, mais l'ordre dans lequel cela se produira est laissé à la discrétion du compilateur. Par conséquent, le code peut très bien imprimer à la fois «1, 2» et vice versa «2, 1». En général, écrire un tel code est hautement indésirable, s'il doit être construit par au moins deux compilateurs, il est possible de tirer dans le pied. Et de nombreux compilateurs donneront un avertissement ici.

En effet, si vous utilisez Clang , vous pouvez obtenir "1, 2".

Et si vous utilisez GCC , vous pouvez obtenir "2, 1".

Il était une fois nous avons essayé le compilateur MSVC, et il a également produit "2, 1". Aucun signe de problème.

Récemment, dans un but tiers général, il a de nouveau été nécessaire de compiler ce code à l'aide de Visual C ++ moderne et de l'exécuter. Assemblé sous la version Release avec l'optimisation / O2 activée . Et, comme on dit, ils ont trouvé l'aventure de leur propre chef :). Que pensez-vous arrivé? Ha! Voici quoi: "1, 1".

Pensez donc à ce que vous voulez. Il s'avère que la question est encore plus profonde et plus confuse. Nous ne nous attendions pas à ce que cela se produise.

Étant donné que la norme C ++ ne réglemente en aucune façon le calcul des arguments, le compilateur interprète ce type de comportement non spécifié d'une manière très particulière. Jetons un coup d'œil au code assembleur généré par le compilateur MSVC 19.25 (Microsoft Visual Studio Community 2019, version 16.5.1), la version du drapeau de la norme de langage est '/ std: c ++ 14':


Formellement, l'optimiseur a transformé le code ci-dessus en ce qui suit:

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

Du point de vue du compilateur, une telle optimisation ne modifie pas le comportement observé du programme. En regardant cela, vous commencez à comprendre que, pour une bonne raison, la norme C ++ 11, en plus des pointeurs intelligents, a également ajouté la fonction «magique» make_shared (et C ++ 14 a également ajouté make_unique ). Un tel exemple inoffensif, et peut également "casser du bois de chauffage":

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 } });
}

Un compilateur astucieux peut transformer cela en l'ordre de calcul suivant (le même MSVC , par exemple):

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

Si le deuxième appel au nouvel opérateur lève une exception, nous obtenons alors une fuite de mémoire.

Mais revenons au sujet d'origine. Malgré le fait que tout allait bien du point de vue du compilateur, nous étions toujours sûrs que la sortie «1, 1» serait incorrecte pour tenir compte du comportement attendu par le développeur. Et puis nous avons essayé de compiler le code source avec le compilateur MSVC avec l'indicateur de version standard '/ std: c ++ 17'. Et tout commence à fonctionner comme prévu et "2, 1" est imprimé. Jetez un œil au code assembleur:


Tout est juste, le compilateur a passé les valeurs 2 et 1. Mais pourquoi tout a-t-il changé si radicalement? Il s'avère que ce qui suit a été ajouté à la norme C ++ 17:

L'expression postfixe est séquencée avant chaque expression dans la liste d'expressions et tout argument par défaut. L'initialisation d'un paramètre, y compris chaque calcul de valeur associé et chaque effet secondaire, est séquencée de façon indéterminée par rapport à celle de tout autre paramètre.

Le compilateur a toujours le droit de calculer les arguments dans un ordre arbitraire, mais maintenant, à partir de la norme C ++ 17, il n'a le droit de commencer à calculer l'argument suivant et ses effets secondaires qu'à partir du moment où tous les calculs et effets secondaires de l'argument précédent sont terminés.

Soit dit en passant, si vous compilez le même exemple avec des pointeurs intelligents avec l'indicateur '/ std: c ++ 17', alors tout va bien - l'utilisation de std :: make_unique est maintenant facultative.

Voici une autre mesure de la profondeur de la question. Il y a une théorie, mais il y a de la pratique sous la forme d'un compilateur spécifique ou d'une interprétation différente de la norme :). Le monde du C ++ est toujours plus complexe et inattendu qu'il n'y paraît.

Si quelqu'un peut expliquer plus précisément ce qui se passe, veuillez nous le dire dans les commentaires. Il faut enfin comprendre la question pour en savoir au moins nous-mêmes la réponse lors de l'entretien! :)

Voici une histoire tellement informative. Nous espérons que c'était intéressant, et vous partagez votre opinion sur ce sujet. Et nous recommandons d'utiliser autant que possible le standard de langage le plus moderne, afin d'être moins surpris que les compilateurs d'optimisation actuels puissent le faire. Mieux encore, n’écrivez pas du tout ce code :).

PS Nous pouvons dire que nous avons «éclairé la question», et maintenant elle devra être supprimée du questionnaire. Nous ne voyons pas l'intérêt de cela. Si une personne n'est pas trop paresseuse pour étudier nos publications avant une entrevue, lit ce matériel puis l'utilise, alors il est bien fait et recevra à juste titre un signe plus :).


Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Andrey Karpov, Phillip Khandeliants. Jusqu'où va le trou du lapin, ou entrevues d'emploi C ++ chez PVS-Studio .

All Articles