web-dev-qa-db-fra.com

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

Si je passe le code suivant dans 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 std::unique_ptr n'est pas copiable:

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

Est-ce que GCC a bien essayé de copier les pointeurs de la liste des initialiseurs?

83

Le synopsis de <initializer_list> dans 18.9 indique raisonnablement que les éléments d’une liste d’initialiseurs sont toujours transmis via const-reference. Malheureusement, il ne semble pas y avoir de moyen d'utiliser move-sémantique dans les éléments de la liste d'initialisation dans la révision en cours 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
42
Kerrek SB

Edit: Comme @Johannes ne semble pas vouloir publier la meilleure solution en guise de 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_iterator déplacent l'élément pointé lors du déréférencement.


Réponse originale: Nous allons utiliser un petit type d'assistance 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 ceci:

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

Le initializer_list<rref_wrapper<move_only>> créé par la commande brace-init-list ({...}) ne sera pas converti en initializer_list<move_only> pris par le vector<move_only>. 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());
53
Xeo

Comme mentionné dans d'autres réponses, le comportement de std::initializer_list consiste à conserver les objets par valeur et à ne pas permettre leur déplacement, ce qui n'est donc pas possible. Voici une solution de contournement possible, en utilisant un appel de fonction dans lequel 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, multi_emplace(foos, {}); échoue car il ne peut pas en déduire le type pour {}. Par conséquent, pour que les objets soient construits par défaut, vous devez répéter le nom de la classe. (ou utilisez vector::resize)

8
M.M

En utilisant l'astuce de std::make_move_iterator() de Johannes Schaub 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 !!
}

Voir en direct sur Colir.

Peut-être que quelqu'un peut utiliser la supercherie de std::make_array() pour permettre à make_vector() de le faire directement, mais je n'ai pas vu comment (plus précisément, j'ai essayé ce que je pensais devoir fonctionner, puis échoué). Dans tous les cas, le compilateur devrait être en mesure de transformer le tableau en vecteur, comme le fait Clang avec O2 sur GodBolt.

0
metal