111 votes

initializer_list et déplacer la sémantique

Suis-je autorisé à déplacer des éléments d'un std::initializer_list<T> ?

 #include <initializer_list>
#include <utility>

template<typename T>
void foo(std::initializer_list<T> list)
{
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        bar(std::move(*it));   // kosher?
    }
}
 

Etant donné que std::intializer_list<T> nécessite une attention particulière du compilateur et n'a pas de sémantique de valeur comme les conteneurs normaux de la bibliothèque standard C ++, je préfère être prudent que désolé et demander.

106voto

Potatoswatter Points 70305

Non, ça ne fonctionne pas comme prévu, vous aurez toujours une copie. Je suis assez surpris par cela, comme je l'avais pensé qu' initializer_list permettaient de tenir un tableau de temporaires jusqu'à ce qu'ils étaient en move'd.

begin et end pour initializer_list rendement const T *, de sorte que le résultat de l' move dans votre code est - T const && - immuable référence rvalue. Une telle expression ne peut pas véritablement être déplacé. Il va se lier à un paramètre de fonction de type T const & parce que rvalues ne se lier à const lvalue références, et vous verrez toujours la sémantique de copie.

Probablement la raison pour cela est que le compilateur peut choisir de faire l' initializer_list statiquement-initialisé constante, mais il semble qu'il serait plus propre de faire son type initializer_list ou const initializer_list au compilateur de la discrétion, de sorte que l'utilisateur ne sait pas s'attendre à un const ou mutable résultat d' begin et end. Mais c'est juste mon sentiment, il y a probablement une bonne raison je me suis trompé.

23voto

Nicol Bolas Points 133791
bar(std::move(*it));   // kosher?

Pas dans le sens que vous souhaitez. Vous ne pouvez pas déplacer un const objet. Et std::initializer_list seulement fournit const l'accès à ses éléments. Donc, le type d' it est const T *.

Votre tentative d'appel d' std::move(*it) entraînera, dans une l-value. C'est à dire: une copie.

std::initializer_list références statique de la mémoire. C'est ce que la classe est destinée. Vous ne pouvez pas vous déplacer à partir de la mémoire statique, car le mouvement implique un changement. Vous pouvez uniquement copier.

3voto

Marc van Leeuwen Points 803

Cela ne fonctionne pas comme indiqué, car list.begin() type const T *, et il n'y a aucun moyen que vous pouvez déplacer à partir d'un objet constant. La langue concepteurs probablement fait que c'est dans le but de permettre à l'initialiseur listes contiennent par exemple des constantes de chaîne, à partir de laquelle il serait inapproprié de se déplacer.

Toutefois, si vous êtes dans une situation où vous savez que l'initialiseur liste contient rvalue expressions (ou si vous voulez forcer l'utilisateur à écrire ces) puis il y a un truc qui va faire fonctionner (j'ai été inspiré par la réponse par Sumant pour cela, mais la solution est plus simple que ça). Vous avez besoin des éléments stockés dans les initialiser la liste pour ne pas être T valeurs, mais les valeurs qui encapsulent T&&. Alors, même si ces valeurs elles-mêmes sont const qualifiés, ils peuvent toujours récupérer modifiable rvalue.

template<typename T>
  class rref_capture
{
  T* ptr;
public:
  rref_capture(T&& x) : ptr(&x) {}
  operator T&& () const { return std::move(*ptr); } // restitute rvalue ref
};

Maintenant, au lieu de déclarer un initializer_list<T> argument, vous déclarez uninitializer_list<rref_capture<T> > argument. Voici un exemple concret, impliquant un vecteur d' std::unique_ptr<int> des pointeurs intelligents, pour qui seule la sémantique de déplacement est défini (de sorte que ces objets eux-mêmes ne peuvent jamais être stockés dans une liste d'initialiseur); cependant, l'initialiseur de la liste ci-dessous compile sans problème.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > l)
    : data(l.begin(),l.end())
  {}
  uptr_vec& operator=(const uptr_vec&) = delete;
  int operator[] (size_t index) const { return *data[index]; }
};

int main()
{
  std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4));
  uptr_vec v { std::move(a), std::move(b), std::move(c) };
  std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl;
}

Une question nécessite une réponse: si les éléments de la liste d'initialiseur doit être vrai prvalues (dans l'exemple, ils sont xvalues), la langue s'assurer que la durée de vie de l'correspondant temporaires s'étend jusqu'au point où ils sont utilisés? Franchement, je ne pense pas que la section correspondante de l'article 8.5 de la norme traite de cette question. Cependant, la lecture de 1.9:10, il semblerait que la pleine expression dans tous les cas, englobe l'utilisation de la liste d'initialiseur, donc je pense qu'il n'y a pas de danger, en balançant les références rvalue.

-1voto

Sumant Points 2294

Considérez le in<T> idiome décrit sur cpptruths . L'idée est de déterminer lvalue / rvalue au moment de l'exécution, puis d'appeler move ou copie-construction. in<T> détectera rvalue / lvalue même si l'interface standard fournie par initializer_list est une référence const.

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