initializer_list et déplacer la sémantique


97

Suis-je autorisé à déplacer des éléments hors 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?
    }
}

Puisque 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 prévenir que guérir et demander.


Le langage de base définit que les objets référencés par an ne initializer_list<T>sont pas -const. Comme, initializer_list<int>fait référence aux intobjets. Mais je pense que c'est un défaut - il est prévu que les compilateurs puissent allouer statiquement une liste en mémoire morte.
Johannes Schaub - litb

Réponses:


89

Non, cela ne fonctionnera pas comme prévu; vous obtiendrez toujours des copies. Je suis assez surpris par cela, car je pensais que cela initializer_listexistait pour garder un éventail de temporaires jusqu'à ce qu'ils le soient move.

beginet endpour le initializer_listretour const T *, le résultat de movedans votre code est T const &&- une référence rvalue immuable. Une telle expression ne peut pas être déplacée de manière significative. Il se liera à un paramètre de fonction de type T const &car rvalues ​​se lie aux références const lvalue et vous verrez toujours la sémantique de copie.

La raison en est probablement que le compilateur peut choisir de créer initializer_listune constante initialisée statiquement, mais il semble qu'il serait plus propre de créer son type initializer_listou const initializer_listà la discrétion du compilateur, de sorte que l'utilisateur ne sait pas s'il doit s'attendre à un constou mutable résultent de beginet end. Mais c'est juste mon instinct, il y a probablement une bonne raison pour laquelle je me trompe.

Mise à jour: j'ai écrit une proposition ISO pour la initializer_listprise en charge des types de déplacement uniquement. Ce n'est qu'un premier projet, et il n'est encore implémenté nulle part, mais vous pouvez le voir pour une analyse plus approfondie du problème.


11
Au cas où ce ne serait pas clair, cela signifie toujours que l'utilisation std::moveest sûre, sinon productive. (Sauf les T const&&constructeurs de mouvement.)
Luc Danton

Je ne pense pas que vous puissiez faire toute l'argumentation const std::initializer_list<T>ou simplement std::initializer_list<T>d'une manière qui ne cause pas souvent de surprises. Considérez que chaque argument dans le initializer_listpeut être l'un constou l' autre et cela est connu dans le contexte de l'appelant, mais le compilateur doit générer une seule version du code dans le contexte de l'appelé (c'est-à-dire qu'à l'intérieur, fooil ne sait rien des arguments que l'appelant passe)
David Rodríguez - dribeas

1
@David: Bon point, mais il serait quand même utile qu'une std::initializer_list &&surcharge fasse quelque chose, même si une surcharge sans référence est également requise. Je suppose que ce serait encore plus déroutant que la situation actuelle, qui est déjà mauvaise.
Potatoswatter

1
@JBJansen Il ne peut pas être piraté. Je ne vois pas exactement ce que ce code est censé accomplir par rapport à initializer_list, mais en tant qu'utilisateur, vous ne disposez pas des autorisations nécessaires pour en sortir. Le code sécurisé ne le fera pas.
Potatoswatter

1
@Potatoswatter, commentaire tardif, mais quel est le statut de la proposition. Y a-t-il une chance lointaine qu'il puisse en faire C ++ 20?
WhiZTiM

20
bar(std::move(*it));   // kosher?

Pas de la manière dont vous l'entendez. Vous ne pouvez pas déplacer un constobjet. Et std::initializer_listne donne constaccès qu'à ses éléments. Donc, le type de itest const T *.

Votre tentative d'appel std::move(*it)n'entraînera qu'une valeur L. IE: une copie.

std::initializer_listfait référence à la mémoire statique . C'est à cela que sert la classe. Vous ne pouvez pas sortir de la mémoire statique, car le mouvement implique de la changer. Vous ne pouvez en copier que.


Une valeur x const est toujours une valeur x et fait initializer_listréférence à la pile si cela est nécessaire. (Si le contenu n'est pas constant, il est toujours thread-safe.)
Potatoswatter

5
@Potatoswatter: Vous ne pouvez pas vous déplacer à partir d'un objet constant. L' initializer_listobjet lui-même peut être une valeur x, mais son contenu (le tableau réel de valeurs vers lequel il pointe) l'est const, car ces contenus peuvent être des valeurs statiques. Vous ne pouvez tout simplement pas vous déplacer du contenu d'un fichier initializer_list.
Nicol Bolas

Voir ma réponse et sa discussion. Il a déplacé l'itérateur déréférencé, produisant une valeur constx. movepeut ne pas avoir de sens, mais il est légal et même possible de déclarer un paramètre qui accepte exactement cela. Si le déplacement d'un type particulier s'avère être un non-op, cela peut même fonctionner correctement.
Potatoswatter

1
@Potatoswatter: La norme C ++ 11 utilise beaucoup de langage pour s'assurer que les objets non temporaires ne sont pas réellement déplacés sauf si vous utilisez std::move. Cela garantit que vous pouvez dire à partir de l'inspection quand une opération de déplacement se produit, car elle affecte à la fois la source et la destination (vous ne voulez pas que cela se produise implicitement pour les objets nommés). Pour cette raison, si vous utilisez std::movedans un endroit où une opération de déplacement ne se produit pas (et aucun mouvement réel ne se produira si vous avez une valeur constx), alors le code est trompeur. Je pense que c'est une erreur std::moved'être appelable sur un constobjet.
Nicol Bolas

1
Peut-être, mais je prendrai encore moins d'exceptions aux règles sur la possibilité d'un code trompeur. Quoi qu'il en soit, c'est exactement pourquoi j'ai répondu "non" même si c'est légal, et le résultat est une valeur x même si elle ne sera liée qu'en tant que valeur constante. Pour être honnête, j'ai déjà eu un bref flirt avec const &&dans une classe garbage collection avec des pointeurs gérés, où tout ce qui était pertinent était mutable et le déplacement déplaçait la gestion du pointeur mais n'affectait pas la valeur contenue. Il y a toujours des cas extrêmes délicats: v).
Potatoswatter

2

Cela ne fonctionnera pas comme indiqué, car list.begin()a un type const T *et il n'y a aucun moyen de vous déplacer à partir d'un objet constant. Les concepteurs de langage ont probablement fait cela pour permettre aux listes d'initialiseurs de contenir par exemple des constantes de chaîne, à partir desquelles il serait inapproprié de se déplacer.

Cependant, si vous êtes dans une situation où vous savez que la liste d'initialisation contient des expressions rvalue (ou que vous voulez forcer l'utilisateur à les écrire), alors il y a une astuce qui le fera fonctionner (j'ai été inspiré par la réponse de Sumant pour ceci, mais la solution est bien plus simple que celle-là). Vous avez besoin que les éléments stockés dans la liste d'initialisation ne soient pas des Tvaleurs, mais des valeurs qui encapsulent T&&. Ensuite, même si ces valeurs elles-mêmes sont constqualifiées, elles peuvent toujours récupérer une rvalue modifiable.

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 un initializer_list<rref_capture<T> >argument. Voici un exemple concret, impliquant un vecteur de std::unique_ptr<int>pointeurs intelligents, pour lequel seule la sémantique de déplacement est définie (donc ces objets eux-mêmes ne peuvent jamais être stockés dans une liste d'initialisation); pourtant la liste des initialiseurs ci-dessous se 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'initialisation doivent être de vraies valeurs prvalues ​​(dans l'exemple ce sont des valeurs x), le langage garantit-il que la durée de vie des temporels correspondants s'étend jusqu'au point où ils sont utilisés? Franchement, je ne pense pas que l'article 8.5 pertinent de la norme traite du tout de cette question. Cependant, à la lecture de 1.9: 10, il semblerait que l' expression complète pertinente dans tous les cas englobe l'utilisation de la liste d'initialiseurs, donc je pense qu'il n'y a aucun danger de suspendre les références rvalue.


Constantes de chaîne? Comme "Hello world"? Si vous vous en éloignez, vous copiez simplement un pointeur (ou liez une référence).
dyp le

1
"Une question nécessite une réponse" Les initialiseurs à l'intérieur {..}sont liés à des références dans le paramètre de fonction de rref_capture. Cela ne prolonge pas leur durée de vie, ils sont toujours détruits à la fin de la pleine expression dans laquelle ils ont été créés.
dyp le

Par commentaire de TC d'une autre réponse: Si vous avez plusieurs surcharges du constructeur, encapsulez lestd::initializer_list<rref_capture<T>> dans un trait de transformation de votre choix - disons std::decay_t- pour bloquer les déductions indésirables.
Réintégrer Monica

2

J'ai pensé qu'il pourrait être instructif d'offrir un point de départ raisonnable pour une solution de contournement.

Commentaires en ligne.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const -> result_type
  {
    result_type result;
    result.reserve(sizeof...(Ts));
    using expand = int[];
    void(expand {
      0,
      (result.push_back(std::forward<Ts>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}

La question était de savoir si un initializer_listpeut être déplacé, pas si quelqu'un avait des solutions de contournement. En outre, le principal argument de vente de initializer_listest qu'il est uniquement basé sur le type d'élément, pas sur le nombre d'éléments, et ne nécessite donc pas que les destinataires soient également modèles - et cela perd complètement cela.
underscore_d

1
@underscore_d vous avez absolument raison. Je considère que le partage des connaissances liées à la question est une bonne chose en soi. Dans ce cas, cela a peut-être aidé le PO et peut-être pas - il n'a pas répondu. Plus souvent qu'autrement, cependant, le PO et d'autres accueillent des éléments supplémentaires liés à la question.
Richard Hodges

Bien sûr, cela peut en effet aider les lecteurs qui veulent quelque chose comme initializer_listmais ne sont pas soumis à toutes les contraintes qui le rendent utile. :)
underscore_d

@underscore_d laquelle des contraintes ai-je négligée?
Richard Hodges

Tout ce que je veux dire, c'est que initializer_list(via la magie du compilateur) évite d'avoir à modéliser des fonctions sur le nombre d'éléments, ce qui est intrinsèquement requis par les alternatives basées sur des tableaux et / ou des fonctions variadiques, limitant ainsi la gamme de cas où ces derniers sont utilisables. D'après ce que j'ai compris, c'est précisément l'une des principales raisons de l'avoir initializer_list, donc cela semblait intéressant de le mentionner.
underscore_d

0

Cela ne semble pas autorisé dans la norme actuelle comme déjà répondu . Voici une autre solution de contournement pour obtenir quelque chose de similaire, en définissant la fonction comme variadique au lieu de prendre une liste d'initialiseurs.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

Les modèles Variadic peuvent gérer les références de valeur r de manière appropriée, contrairement à initializer_list.

Dans cet exemple de code, j'ai utilisé un ensemble de petites fonctions d'assistance pour convertir les arguments variadiques en un vecteur, pour le rendre similaire au code d'origine. Mais bien sûr, vous pouvez écrire directement une fonction récursive avec des modèles variadiques.


La question était de savoir si un initializer_listpeut être déplacé, pas si quelqu'un avait des solutions de contournement. En outre, le principal argument de vente de initializer_listest qu'il est uniquement basé sur le type d'élément, pas sur le nombre d'éléments, et ne nécessite donc pas que les destinataires soient également modèles - et cela perd complètement cela.
underscore_d

0

J'ai une implémentation beaucoup plus simple qui utilise une classe wrapper qui agit comme une balise pour marquer l'intention de déplacer les éléments. Il s'agit d'un coût de compilation.

La classe wrapper est conçue pour être utilisée de la manière std::moveutilisée, il suffit de la remplacer std::movepar move_wrapper, mais cela nécessite C ++ 17. Pour les spécifications plus anciennes, vous pouvez utiliser une méthode de générateur supplémentaire.

Vous devrez écrire des méthodes / constructeurs de générateur qui acceptent les classes wrapper à l'intérieur initializer_listet déplacer les éléments en conséquence.

Si vous avez besoin que certains éléments soient copiés au lieu d'être déplacés, créez une copie avant de la transmettre initializer_list.

Le code doit être auto-documenté.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}

0

Au lieu d'utiliser a std::initializer_list<T>, vous pouvez déclarer votre argument en tant que référence de tableau rvalue:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

Voir l'exemple utilisant std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6


-1

Considérez l' 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 copy-construction. in<T>détectera rvalue / lvalue même si l'interface standard fournie par initializer_list est const reference.


4
Pourquoi diable voudriez-vous déterminer la catégorie de valeur au moment de l'exécution alors que le compilateur la connaît déjà?
fredoverflow

1
Veuillez lire le blog et laissez-moi un commentaire si vous n'êtes pas d'accord ou si vous avez une meilleure alternative. Même si le compilateur connaît la catégorie de valeur, initializer_list ne la conserve pas car il n'a que des itérateurs const. Vous devez donc «capturer» la catégorie de valeur lorsque vous construisez la liste d'initialisation et la transmettre afin que la fonction puisse l'utiliser à sa guise.
Sumant

5
Cette réponse est fondamentalement inutile sans suivre le lien, et les réponses SO devraient être utiles sans suivre les liens.
Yakk - Adam Nevraumont

1
@Sumant [copiant mon commentaire d'un article identique ailleurs] Ce désordre énorme fournit-il réellement des avantages mesurables en termes de performances ou d'utilisation de la mémoire, et si c'est le cas, une quantité suffisamment importante de ces avantages pour compenser adéquatement à quel point cela a l'air terrible et le fait qu'il prend environ une heure pour comprendre ce qu'il essaie de faire? J'en doute un peu.
underscore_d
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.