web-dev-qa-db-fra.com

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?
    }
}

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 être prudent que désolé et demander.

83
fredoverflow

Non, cela ne fonctionnera pas comme prévu; vous obtiendrez toujours des copies. Je suis assez surpris par cela, car j'avais pensé que initializer_list existait pour conserver un tableau de temporaires jusqu'à ce qu'ils soient move 'd.

begin et end pour initializer_list revenir const T *, donc le résultat de move dans votre code est T const && - une référence de valeur immuable. Une telle expression ne peut pas être déplacée de manière significative. Il sera lié à un paramètre de fonction de type T const & car les valeurs r se lient 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 faire le initializer_list une constante initialisée statiquement, mais il semble qu'il serait plus propre de faire son type initializer_list ou const initializer_list à la discrétion du compilateur, de sorte que l'utilisateur ne sait pas s'il doit s'attendre à un résultat const ou mutable de begin et end. Mais c'est juste mon instinct, probablement il y a une bonne raison pour laquelle je me trompe.

Mise à jour: J'ai écrit ne proposition ISO pour initializer_list prise 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.

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

Pas de la manière que vous envisagez. Vous ne pouvez pas déplacer un objet const. Et std::initializer_list Ne fournit que const accès à ses éléments. Le type de it est donc const T *.

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

std::initializer_list Fait référence à statique mémoire. C'est à ça que sert le cours. Vous ne pouvez pas déplacer à partir de la mémoire statique, car le mouvement implique de le changer. Vous ne pouvez que copier à partir de celui-ci.

17
Nicol Bolas

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));
}
2
Richard Hodges

Cela ne fonctionnera pas comme indiqué, car list.begin() a le type const T *, et vous ne pouvez pas vous déplacer depuis un objet constant. Les concepteurs de langage l'ont probablement fait pour permettre aux listes d'initialisation de contenir par exemple des constantes de chaîne, dont 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 si vous voulez forcer l'utilisateur à les écrire), 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 devez que les éléments stockés dans la liste d'initialisation ne soient pas des valeurs T, mais des valeurs qui encapsulent T&&. Ensuite, même si ces valeurs elles-mêmes sont qualifiées const, elles peuvent toujours récupérer une valeur r 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 uninitializer_list<rref_capture<T> > argument. Voici un exemple concret, impliquant un vecteur de std::unique_ptr<int> pointeurs intelligents, pour lesquels seule la sémantique de déplacement est définie (de sorte que ces objets eux-mêmes ne peuvent jamais être stockés dans une liste d'initialisation); pourtant la liste d'initialisation 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 pr (dans l'exemple ce sont des valeurs x), le langage garantit-il que la durée de vie des temporaires correspondants s'étend jusqu'au point où ils sont utilisés? Franchement, je ne pense pas que la section 8.5 pertinente de la norme traite du tout de cette question. Cependant, en lisant 1.9: 10, il semblerait que le expression complète pertinent englobe dans tous les cas l'utilisation de la liste d'initialisation, donc je pense qu'il n'y a aucun danger de balancer les références de valeur.

2
Marc van Leeuwen

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::move est utilisé, il suffit de remplacer std::move avec move_wrapper, mais cela nécessite C++ 17. Pour les spécifications plus anciennes, vous pouvez utiliser une méthode de création supplémentaire.

Vous devrez écrire des méthodes/constructeurs de constructeur qui acceptent les classes wrapper dans initializer_list et déplacer les éléments en conséquence.

Si vous avez besoin de copier certains éléments au lieu de les déplacer, construisez une copie avant de la passer à 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
bumfo

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

#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 directement écrire une fonction récursive avec des modèles variadiques.

0
Hiroshi Ichikawa