Чи можу я перерахувати-ініціалізувати вектор типу лише для переміщення?


95

Якщо я передаю наступний код через свій знімок GCC 4.7, він намагається скопіювати unique_ptrs у вектор.

#include <vector>
#include <memory>

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

Очевидно, що це не може працювати, оскільки std::unique_ptrне можна скопіювати:

помилка: використання видаленої функції 'std :: unique_ptr <_Tp, _Dp> :: unique_ptr (const std :: unique_ptr <_Tp, _Dp> &) [з _Tp = int; _Dp = std :: default_delete; std :: unique_ptr <_Tp, _Dp> = std :: unique_ptr] '

Чи правильно GCC намагається скопіювати вказівники зі списку ініціалізатора?


Visual Studio і clang мають однакову поведінку
Жан-Саймон Броху

Відповіді:


46

Зміст <initializer_list>в 18.9 робить досить зрозумілим, що елементи списку ініціалізатора завжди передаються через const-reference. На жаль, схоже, не існує жодного способу використання семантики переміщення в елементах списку ініціалізатора в поточній редакції мови.

Зокрема, ми маємо:

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

4
Розглянемо ідіому в <T>, описану в cpptruths ( cpptruths.blogspot.com/2013/09/… ). Ідея полягає у визначенні lvalue / rvalue під час виконання, а потім виклик переміщення або копіювання. в <T> виявить rvalue / lvalue, навіть незважаючи на те, що стандартний інтерфейс, наданий Initializer_list, є посиланням const.
Sumant

3
@Sumant Не здається мені настільки "ідіоматичним": чи не натомість це чистий UB? як може бути не просто ітератор, а самі основні елементи const, які неможливо відкинути у чітко сформованій програмі.
underscore_d

62

Редагувати: Оскільки @Johannes, схоже, не хоче розміщувати найкраще рішення як відповідь, я просто зроблю це.

#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))};
}

Ітератори, що повертаються, std::make_move_iteratorпереміщуватимуть елемент, що вказує, при розмежуванні.


Оригінальна відповідь: Ми використаємо тут маленький помічник:

#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;

На жаль, прямий код тут не буде працювати:

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

Оскільки стандарт з будь-якої причини не визначає конструктор перетворюючої копії таким чином:

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

initializer_list<rref_wrapper<move_only>>Створений брекет-Init-лист ( {...}) не конвертувати в initializer_list<move_only>тому , що vector<move_only>відбувається. Отже, нам потрібна двоетапна ініціалізація тут:

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());

1
Ах ... це аналог rvalue std::ref, не? Можливо, його слід назвати std::rref.
Kerrek SB

17
Тепер, мабуть, цього не слід залишати, не згадуючи в коментарі :) 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));.
Йоханнес Шауб - літб

1
@ Johanes: Іноді мені просто не під силу прості рішення. Хоча я повинен визнати, я ще не турбувався з цими move_iterators.
Xeo

2
@ Johanes: Крім того, чому це не відповідь? :)
Xeo

1
@JohanLundberg: Я вважав би це проблемою якості, але я не розумію, чому це не могло цього зробити. Stdlib VC ++, наприклад, відправлення тегів на основі категорії ітераторів і використовує std::distanceдля ітераторів вперед чи краще та std::move_iteratorадаптує базову категорію ітератора. У будь-якому випадку, гарне та стисле рішення. Опублікувати це як відповідь, можливо?
Xeo

10

Як зазначалося в інших відповідях, поведінка користувача std::initializer_listполягає в тому, щоб утримувати предмети за значенням і не дозволяти переміщатися, тож це неможливо. Ось одне з можливих обхідних шляхів, використовуючи виклик функції, де ініціалізатори подаються як різні варіанти аргументів:

#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{});
}

На жаль, multi_emplace(foos, {});не вдається, оскільки він не може вивести тип для {}, тому для об'єктів, які будуються за замовчуванням, вам доведеться повторити ім'я класу. (або використовувати vector::resize)


4
Рекурсивне розширення пакета може бути замінено фіктивним маніпулятором оператора коми, щоб зберегти пару рядків коду
MM

0

Використовуючи трюк Йоганнеса Шауба std::make_move_iterator()з std::experimental::make_array(), ви можете використовувати допоміжну функцію:

#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 !!
}

Перегляньте це у прямому ефірі Coliru.

Можливо, хтось може скористатися std::make_array()хитрощами, щоб дозволити make_vector()робити свою справу безпосередньо, але я не зрозумів, як (точніше, я спробував те, що, на мою думку, мало спрацювати, не вдалося і рухався далі). У будь-якому випадку компілятор повинен мати можливість вставити масив у векторне перетворення, як це робить Clang із увімкненим O2 GodBolt.


-1

Як вже зазначалося, неможливо ініціалізувати вектор типу "лише переміщення" за допомогою списку ініціалізаторів. Рішення, спочатку запропоноване @Johannes, чудово працює, але я маю іншу ідею ... Що робити, якщо ми не створимо тимчасовий масив, а потім перемістимо елементи звідти у вектор, а використаємо розміщення newдля ініціалізації цього масиву вже замість блок пам'яті вектора?

Ось моя функція для ініціалізації вектора unique_ptrs за допомогою пакета аргументів:

#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;
    }
}

Це жахлива ідея. Розміщення new - це не молоток, це інструмент тонкої точності. result.data()не є вказівником на деяку випадкову пам'ять. Це вказівник на об’єкт . Подумайте, що відбувається з цим бідним предметом, коли ви розміщуєте над ним новий.
Р. Мартіньо Фернандес

Крім того, масивна форма розміщення new насправді не придатна для використання stackoverflow.com/questions/8720425/…
Р. Мартіньо Фернандес,

@R. Мартіньо Фернандес: дякую, що вказав, що розміщення нових для масивів не буде працювати. Тепер я бачу, чому це була погана ідея.
Гарт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.