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> требует особого внимания компилятора и не имеет семантики значений, как обычные контейнеры стандартной библиотеки C++, я бы предпочел быть в безопасности, чем сожалеть и спрашивать.

9 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вместо использования std::initializer_list<T>, вы можете объявить свой аргумент как ссылку на массив rvalue:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

См. Пример использования std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6

Это один из случаев, когдаconst_castхорошо использовать

      Sum::Sum(std::initializer_list<Valuable>&& l)
{
    for (auto& a : l)
    {
        auto&& arg = std::move(const_cast<Valuable&>(a));
        Add(std::move(arg));
    }
}

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

#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-значения соответствующим образом, в отличие от initializer_list.

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

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

Класс-оболочка предназначен для использования в std::move используется, просто замените std::move с move_wrapper, но для этого требуется C++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'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)
}

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

Другие вопросы по тегам