Puis-je lister un vecteur de type déplacement uniquement?

95

Si je passe le code suivant via mon instantané GCC 4.7, il essaie de copier le unique_ptrs dans le vecteur.

#include <vector>
#include <memory>

int main() {
    using move_only = std::unique_ptr<int>;
    std::vector<move_only> v { move_only(), move_only(), move_only() };
}

Évidemment, cela ne peut pas fonctionner car il std::unique_ptrn'est pas copiable:

erreur: utilisation de la fonction supprimée 'std :: unique_ptr <_Tp, _Dp> :: unique_ptr (const std :: unique_ptr <_Tp, _Dp> &) [with _Tp = int; _Dp = std :: default_delete; std :: unique_ptr <_Tp, _Dp> = std :: unique_ptr] '

GCC a-t-il raison d'essayer de copier les pointeurs de la liste d'initialisation?

R. Martinho Fernandes
la source
Visual Studio et clang ont le même comportement
Jean-Simon Brochu

Réponses:

46

Le synopsis de la version <initializer_list>18.9 indique assez clairement que les éléments d'une liste d'initialisation sont toujours passés via const-reference. Malheureusement, il ne semble pas y avoir de moyen d'utiliser move-semantic dans les éléments de la liste d'initialisation dans la révision actuelle du langage.

Plus précisément, nous avons:

typedef const E& reference;
typedef const E& const_reference;

typedef const E* iterator;
typedef const E* const_iterator;

const E* begin() const noexcept; // first element
const E* end() const noexcept; // one past the last element
Kerrek SB
la source
4
Considérez l'idiome in <T> décrit sur cpptruths ( cpptruths.blogspot.com/2013/09/… ). L'idée est de déterminer lvalue / rvalue au moment de l'exécution, puis d'appeler move ou copy-construction. dans <T> détectera rvalue / lvalue même si l'interface standard fournie par initializer_list est const reference.
Sumant
3
@Sumant Cela ne me semble pas si "idiomatique": n'est-ce pas, au contraire, du pur UB? comme non seulement l'itérateur mais plutôt les éléments sous-jacents eux-mêmes const, qui ne peuvent pas être rejetés dans un programme bien formé.
underscore_d
62

Edit: Puisque @Johannes ne semble pas vouloir publier la meilleure solution comme réponse, je vais le faire.

#include <iterator>
#include <vector>
#include <memory>

int main(){
  using move_only = std::unique_ptr<int>;
  move_only init[] = { move_only(), move_only(), move_only() };
  std::vector<move_only> v{std::make_move_iterator(std::begin(init)),
      std::make_move_iterator(std::end(init))};
}

Les itérateurs renvoyés par std::make_move_iteratordéplaceront l'élément pointé lors du déréférencement.


Réponse originale: nous allons utiliser un petit type d'aide ici:

#include <utility>
#include <type_traits>

template<class T>
struct rref_wrapper
{ // CAUTION - very volatile, use with care
  explicit rref_wrapper(T&& v)
    : _val(std::move(v)) {}

  explicit operator T() const{
    return T{ std::move(_val) };
  }

private:
  T&& _val;
};

// only usable on temporaries
template<class T>
typename std::enable_if<
  !std::is_lvalue_reference<T>::value,
  rref_wrapper<T>
>::type rref(T&& v){
  return rref_wrapper<T>(std::move(v));
}

// lvalue reference can go away
template<class T>
void rref(T&) = delete;

Malheureusement, le code simple ici ne fonctionnera pas:

std::vector<move_only> v{ rref(move_only()), rref(move_only()), rref(move_only()) };

Puisque la norme, pour une raison quelconque, ne définit pas un constructeur de copie de conversion comme celui-ci:

// in class initializer_list
template<class U>
initializer_list(initializer_list<U> const& other);

Le initializer_list<rref_wrapper<move_only>>créé par l'accolade-init-list ( {...}) ne sera pas converti en celui initializer_list<move_only>que vector<move_only>prend. Nous avons donc besoin d'une initialisation en deux étapes ici:

std::initializer_list<rref_wrapper<move_only>> il{ rref(move_only()),
                                                   rref(move_only()),
                                                   rref(move_only()) };
std::vector<move_only> v(il.begin(), il.end());
Xeo
la source
1
Ah ... c'est l'analogue rvalue de std::ref, non? Peut-être qu'il devrait être appelé std::rref.
Kerrek SB
17
Maintenant, je suppose que cela ne devrait pas être laissé sans être mentionné dans un commentaire :) move_only m[] = { move_only(), move_only(), move_only() }; std::vector<move_only> v(std::make_move_iterator(m), std::make_move_iterator(m + 3));.
Johannes Schaub - litb
1
@Johannes: Parfois, ce sont les solutions simples qui m'échappent. Bien que je dois l'admettre, je ne me suis pas encore préoccupé de ceux-là move_iterator.
Xeo
2
@Johannes: Aussi, pourquoi n'est-ce pas une réponse? :)
Xeo
1
@JohanLundberg: Je considérerais cela comme un problème de QoI, mais je ne vois pas pourquoi il ne pourrait pas le faire. Le stdlib de VC ++, par exemple, les distributions de balises basées sur la catégorie de l'itérateur et les utilise std::distancepour les itérateurs avant ou supérieur et std::move_iteratoradapte la catégorie de l'itérateur sous-jacent. Quoi qu'il en soit, solution bonne et concise. Postez-le comme réponse, peut-être?
Xeo
10

Comme mentionné dans d'autres réponses, le comportement de std::initializer_listest de tenir les objets par valeur et de ne pas permettre de sortir, ce n'est donc pas possible. Voici une solution de contournement possible, en utilisant un appel de fonction où les initialiseurs sont donnés sous forme d'arguments variadiques:

#include <vector>
#include <memory>

struct Foo
{
    std::unique_ptr<int> u;
    int x;
    Foo(int x = 0): x(x) {}
};

template<typename V>        // recursion-ender
void multi_emplace(std::vector<V> &vec) {}

template<typename V, typename T1, typename... Types>
void multi_emplace(std::vector<V> &vec, T1&& t1, Types&&... args)
{
    vec.emplace_back( std::move(t1) );
    multi_emplace(vec, args...);
}

int main()
{
    std::vector<Foo> foos;
    multi_emplace(foos, 1, 2, 3, 4, 5);
    multi_emplace(foos, Foo{}, Foo{});
}

Malheureusement, il multi_emplace(foos, {});échoue car il ne peut pas déduire le type de {}, donc pour que les objets soient construits par défaut, vous devez répéter le nom de la classe. (ou utiliser vector::resize)

MM
la source
4
L'expansion du pack récursif pourrait être remplacée par le piratage de l'opérateur de virgule du tableau factice, pour enregistrer quelques lignes de code
MM
0

En utilisant l'astuce de Johannes Schaub std::make_move_iterator()avec std::experimental::make_array(), vous pouvez utiliser une fonction d'assistance:

#include <memory>
#include <type_traits>
#include <vector>
#include <experimental/array>

struct X {};

template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)) };
}

template<class... T>
auto make_vector( T&& ... t )
    -> std::vector<typename std::common_type<T...>::type>
{
    return make_vector( std::experimental::make_array( std::forward<T>(t)... ) );
}

int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::experimental::make_array( UX{}, UX{}, UX{} ); // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );                   // Ok
    //const auto v1 = std::vector< UX >{ UX{}, UX{}, UX{} };           // !! Error !!
}

Regardez-le en direct Coliru.

Peut-être que quelqu'un peut tirer parti de std::make_array()la supercherie pour lui permettre make_vector()de faire son travail directement, mais je n'ai pas vu comment (plus précisément, j'ai essayé ce que je pensais devoir fonctionner, j'ai échoué et je suis passé à autre chose). Dans tous les cas, le compilateur devrait être en mesure d'insérer le tableau en transformation vectorielle, comme le fait Clang avec O2 activé GodBolt.

métal
la source
-1

Comme il a été souligné, il n'est pas possible d'initialiser un vecteur de type move-only avec une liste d'initialiseurs. La solution proposée à l'origine par @Johannes fonctionne très bien, mais j'ai une autre idée ... Que faire si nous ne créons pas un tableau temporaire puis déplaçons des éléments de là dans le vecteur, mais utilisons le placement newpour initialiser ce tableau déjà à la place du bloc de mémoire du vecteur?

Voici ma fonction pour initialiser un vecteur de unique_ptr's en utilisant un pack d'arguments:

#include <iostream>
#include <vector>
#include <make_unique.h>  /// @see http://stackoverflow.com/questions/7038357/make-unique-and-perfect-forwarding

template <typename T, typename... Items>
inline std::vector<std::unique_ptr<T>> make_vector_of_unique(Items&&... items) {
    typedef std::unique_ptr<T> value_type;

    // Allocate memory for all items
    std::vector<value_type> result(sizeof...(Items));

    // Initialize the array in place of allocated memory
    new (result.data()) value_type[sizeof...(Items)] {
        make_unique<typename std::remove_reference<Items>::type>(std::forward<Items>(items))...
    };
    return result;
}

int main(int, char**)
{
    auto testVector = make_vector_of_unique<int>(1,2,3);
    for (auto const &item : testVector) {
        std::cout << *item << std::endl;
    }
}
Gart
la source
C'est une idée terrible. Le placement neuf n'est pas un marteau, c'est un outil d'une précision fine. result.data()n'est pas un pointeur vers une mémoire aléatoire. C'est un pointeur vers un objet . Pensez à ce qui arrive à ce pauvre objet lorsque vous en placez un nouveau dessus.
R. Martinho Fernandes
De plus, la nouvelle forme de placement de tableau n'est pas vraiment utilisable stackoverflow.com/questions/8720425/...
R. Martinho Fernandes
@R. Martinho Fernandes: merci d'avoir signalé que le placement nouveau pour les tableaux ne fonctionnerait pas. Maintenant, je vois pourquoi c'était une mauvaise idée.
Gart