Initializer_list и семантика перемещения

Мне разрешено перемещать элементы из 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?
    }
}

Так как std::intializer_list<T> требует особого внимания компилятора и не имеет семантики значения, как обычные контейнеры стандартной библиотеки С++, я бы предпочел быть в безопасности, чем сожалеть и спросить.

Ответ 1

Нет, это не сработает по назначению; вы все равно получите копии. Я очень удивлен этим, так как я думал, что initializer_list существует, чтобы хранить массив временных до тех пор, пока они не будут move 'd.

begin и end для initializer_list return const T *, поэтому результат move в вашем коде T const && - неизменяемая ссылка rvalue. Из этого выражения нельзя толкнуть. Он будет привязан к функциональному параметру типа T const &, потому что rvalues ​​связывают ссылки const lvalue, и вы все равно увидите семантику копирования.

Вероятно, причина этого заключается в том, что компилятор может выбрать, чтобы сделать initializer_list статически инициализированную константу, но, похоже, было бы проще сделать ее тип initializer_list или const initializer_list по усмотрению компилятора, поэтому пользователь не знает, ожидать ли const или изменяемый результат из begin и end. Но это только мое чувство кишки, возможно, есть веская причина, я ошибаюсь.

Обновление: Я написал предложение ISO для поддержки initializer_list типов только для перемещения. Это только первый проект, и он еще ничего не реализован, но вы можете увидеть его для большего анализа проблемы.

Ответ 2

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

Не так, как вы намереваетесь. Вы не можете перемещать объект const. И std::initializer_list предоставляет const доступ к своим элементам. Таким образом, тип it равен const T *.

Ваша попытка вызвать std::move(*it) приведет только к l-значению. IE: копия.

std::initializer_list ссылается на статическую память. Для этого нужен класс. Вы не можете перемещаться из статической памяти, потому что движение подразумевает ее изменение. Вы можете копировать только его.

Ответ 3

Это не будет работать, как указано, потому что list.begin() имеет тип const T *, и вы не можете перемещаться из постоянного объекта. Возможно, разработчики языка сделали это так, чтобы списки инициализаторов содержали, например, строковые константы, из которых было бы неуместно перемещаться.

Однако, если вы находитесь в ситуации, когда знаете, что список инициализаторов содержит выражения rvalue (или вы хотите заставить пользователя писать их), тогда есть трюк, который заставит его работать (я был вдохновлен ответом Sumant для этого, но решение проще, чем одно). Вам нужно, чтобы элементы, хранящиеся в списке инициализаторов, не были T, а значениями, которые инкапсулировали T&&. Тогда даже если эти значения сами являются const квалифицированными, они все равно могут получить модифицируемое значение r.

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

Теперь вместо объявления аргумента initializer_list<T> вы объявляете аргумент initializer_list<rref_capture<T> >. Вот конкретный пример, связанный с вектором std::unique_ptr<int> интеллектуальных указателей, для которых определяется только перемещение семантики (поэтому сами эти объекты никогда не могут быть сохранены в списке инициализаторов); но список инициализаторов ниже компилируется без проблем.

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

Один вопрос требует ответа: если элементы списка инициализаторов должны быть истинными prvalues ​​(в примере они являются xvalues), гарантирует ли язык жизни соответствующих временных времен до точки, в которой они используются? Честно говоря, я не думаю, что соответствующий раздел 8.5 стандарта затрагивает эту проблему вообще. Однако, прочитав 1.9: 10, похоже, что соответствующее полное выражение во всех случаях включает в себя использование списка инициализаторов, поэтому я думаю, что нет опасности оборвать ссылки на rvalue.

Ответ 4

Я думал, что было бы поучительно предложить разумную отправную точку для обхода.

Комментарии в строке.

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

Ответ 5

Кажется, в текущем стандарте не разрешено отвечать . Вот еще одно обходное решение для достижения чего-то подобного, определяя функцию как вариационную, вместо того, чтобы принимать список инициализаторов.

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

Шаблоны Variadic могут обрабатывать ссылки r-value соответствующим образом, в отличие от initializer_list.

В этом примере кода я использовал набор небольших вспомогательных функций для преобразования вариативных аргументов в вектор, чтобы сделать его похожим на исходный код. Но, конечно, вы можете написать рекурсивную функцию с вариационными шаблонами напрямую.

Ответ 6

У меня есть намного более простая реализация, которая использует класс-обертку, который действует как тег, чтобы отметить намерение перемещения элементов. Это стоимость времени компиляции.

Класс-обертка предназначен для использования таким же образом, как и std::move, просто замените std::move на move_wrapper, но для этого требуется С++ 17. Для более старых спецификаций вы можете использовать дополнительный метод компоновщика.

Вам нужно написать методы/конструкторы компоновщика, которые принимают классы-оболочки внутри initializer_list и соответственно перемещают элементы.

Если вам нужно скопировать некоторые элементы, а не перемещать их, создайте копию, прежде чем передавать ее в initializer_list.

Код должен быть самодокументированным.

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

Ответ 7

Рассмотрим in<T> идиому, описанную в cpptruths. Идея состоит в том, чтобы определить lvalue/rvalue во время выполнения, а затем вызвать move или copy-construction. in<T> будет определять значение rvalue/lvalue, даже если стандартный интерфейс, предоставляемый параметром initializer_list, является ссылкой на константу.