49 votes

Sémantique des déplacements et évaluation de l'ordre des fonctions

Supposons que j'ai les éléments suivants :

#include <memory>
struct A { int x; };

class B {
  B(int x, std::unique_ptr<A> a);
};

class C : public B {
  C(std::unique_ptr<A> a) : B(a->x, std::move(a)) {}
};

Si je comprends correctement les règles du C++ concernant "l'ordre non spécifié des paramètres de fonction", ce code n'est pas sûr. Si le deuxième argument de B est d'abord construit à l'aide du constructeur de move, puis de a contient maintenant un nullptr et l'expression a->x déclenchera un comportement non défini (probablement une erreur de segmentation). Si le premier argument est construit en premier, alors tout fonctionnera comme prévu.

Si c'était un appel de fonction normal, nous pourrions simplement créer un temporaire :

auto x = a->x
B b{x, std::move(a)};

Mais dans la liste d'initialisation des classes, nous n'avons pas la liberté de créer des variables temporaires.

Supposons que je ne puisse pas changer B Est-ce qu'il y a un moyen possible d'accomplir ce qui précède ? A savoir déréférencer et déplacer un unique_ptr dans la même expression d'appel de fonction sans créer un temporaire ?

Et si vous pouviez changer B mais n'ajoute pas de nouvelles méthodes telles que setX(int) ? Cela aiderait-il ?

Merci.

47voto

Praetorian Points 47122

Utilisez l'initialisation de la liste pour construire B . Les éléments sont alors garantis d'être évalués de gauche à droite.

C(std::unique_ptr<A> a) : B{a->x, std::move(a)} {}
//                         ^                  ^ - braces

Desde §8.5.4/4 [dcl.init.list].

Au sein de la liste d'initialisation d'un liste d'installation entre crochets le Clauses d'initialisation y compris ceux qui résultent des expansions de paquets (14.5.3), sont évalués dans l'ordre dans lequel ils apparaissent. C'est-à-dire que tous les calculs de valeurs et les effets secondaires associés à un paquet donné sont évalués dans l'ordre où ils apparaissent. clause d'initialisation est séquencée avant chaque calcul de valeur et chaque effet secondaire associé à un quelconque clause d'initialisation qui le suit dans la liste, séparée par des virgules, des éléments suivants liste d'initialisation .

31voto

Jarod42 Points 15729

Comme alternative à la réponse de Praetorian, vous pouvez utiliser un constructeur délégué :

class C : public B {
public:
    C(std::unique_ptr<A> a) :
        C(a->x, std::move(a)) // this move doesn't nullify a.
    {}

private:
    C(int x, std::unique_ptr<A>&& a) :
        B(x, std::move(a)) // this one does, but we already have copied x
    {}
};

10voto

La suggestion de Praetorian d'utiliser l'initialisation de liste semble fonctionner, mais elle présente quelques problèmes :

  1. Si l'argument unique_ptr vient en premier, nous n'avons pas de chance.
  2. C'est bien trop facile pour les clients de B d'oublier accidentellement d'utiliser {} au lieu de () . Les concepteurs de B L'interface de l'entreprise nous a imposé ce bug potentiel.

Si nous pouvions changer B, alors peut-être qu'une meilleure solution pour les constructeurs est de toujours passer unique_ptr par référence rvalue au lieu de par valeur.

struct A { int x; };

class B {
  B(std::unique_ptr<A>&& a, int x) : _x(x), _a(std::move(a)) {}
};

Maintenant, nous pouvons utiliser std::move() en toute sécurité.

B b(std::move(a), a->x);
B b{std::move(a), a->x};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X