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


98

Могу ли я перемещать элементы из 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 ++, я бы предпочел перестраховаться, чем извиниться и спросить.


Ядро языка определяет , что объект , на которые ссылается initializer_list<T>являются не -const. Нравится, initializer_list<int>относится к intобъектам. Но я думаю, что это дефект - предполагается, что компиляторы могут статически размещать список в памяти только для чтения.
Йоханнес Шауб - лит

Ответы:


90

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

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

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

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


11
Если это неясно, это все равно означает, что использование std::moveбезопасно, если не продуктивно. (За исключением T const&&конструкторов ходов .)
Люк Дантон

Я не думаю, что вы могли бы привести аргумент целиком const std::initializer_list<T>или просто std::initializer_list<T>так, чтобы не вызывать неожиданностей достаточно часто. Учтите, что каждый аргумент в классе initializer_listможет быть любым constили нет, и это известно в контексте вызывающего, но компилятор должен сгенерировать только одну версию кода в контексте вызываемого (т.е. внутри fooон ничего не знает об аргументах что звонящий проходит)
Дэвид Родригес - дрибэас

1
@David: Хороший замечание, но все равно было бы полезно, чтобы std::initializer_list &&перегрузка что-то делала, даже если также требуется перегрузка без ссылки. Полагаю, это было бы еще более запутанным, чем нынешняя ситуация, которая и так плоха.
Potatoswatter

1
@JBJansen Его нельзя взломать. Я не понимаю, что именно этот код должен выполнять в отношении initializer_list, но как пользователь у вас нет необходимых разрешений для перехода от него. Безопасный код этого не сделает.
Potatoswatter

1
@Potatoswatter, поздний комментарий, но каков статус предложения. Есть ли шанс, что он попадет в C ++ 20?
WhiZTiM

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

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

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

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


Константа xvalue по-прежнему является xvalue и initializer_listссылается на стек, если это необходимо. (Если содержимое непостоянно, оно по-прежнему является потокобезопасным.)
Potatoswatter

5
@Potatoswatter: вы не можете двигаться от постоянного объекта. Сам initializer_listобъект может быть xvalue, но его содержимое (фактический массив значений, на которые он указывает) является const, потому что это содержимое может быть статическими значениями. Вы просто не можете перейти от содержимого файла initializer_list.
Никол Болас

Смотрите мой ответ и его обсуждение. Он переместил разыменованный итератор, constполучив xvalue. moveможет быть бессмысленным, но это законно и даже возможно объявить параметр, который принимает именно это. Если перемещение определенного типа невозможно, оно может даже работать правильно.
Potatoswatter

1
@Potatoswatter: Стандарт C ++ 11 требует большого количества языков, гарантируя, что невременные объекты фактически не перемещаются, если вы не используете std::move. Это гарантирует, что при проверке вы сможете определить, когда происходит операция перемещения, поскольку она влияет как на источник, так и на место назначения (вы не хотите, чтобы это происходило неявно для именованных объектов). Из-за этого, если вы используете std::moveв месте, где операция перемещения не происходит (и фактического перемещения не произойдет, если у вас есть значение constx), тогда код вводит в заблуждение. Я считаю ошибкой std::moveбыть вызванным к constобъекту.
Никол Болас

1
Возможно, но я все равно буду делать меньше исключений из правил из-за возможности введения в заблуждение кода. В любом случае, именно поэтому я ответил «нет», хотя это допустимо, и результатом будет xvalue, даже если он будет связываться только как const lvalue. Честно говоря, у меня уже было небольшое заигрывание с const &&классом, собирающим мусор, с управляемыми указателями, где все относящееся к делу изменялось, а перемещение перемещало управление указателем, но не влияло на содержащееся значение. Всегда есть сложные крайние случаи: v).
Potatoswatter

2

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

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

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

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


Строковые константы? Нравится "Hello world"? Если вы двигаетесь от них, вы просто копируете указатель (или привязываете ссылку).
dyp 07

1
«Один вопрос действительно требует ответа» Инициализаторы внутри {..}привязаны к ссылкам в параметре функции rref_capture. Это не продлевает их срок службы, они все равно уничтожаются в конце полного выражения, в котором они были созданы.
dyp 07

Согласно комментарию TC из другого ответа: если у вас есть несколько перегрузок конструктора, оберните их std::initializer_list<rref_capture<T>>в какой-нибудь признак преобразования по вашему выбору, например, std::decay_tчтобы заблокировать нежелательный вывод.
Unslander Monica

2

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

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

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

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

1
@underscore_d, ты абсолютно прав. Я считаю, что делиться знаниями, связанными с этим вопросом, само по себе хорошо. В этом случае, возможно, это помогло OP, а возможно, нет - он не ответил. Однако чаще всего ОП и другие приветствуют дополнительные материалы, связанные с вопросом.
Ричард Ходжес

Конечно, это действительно может помочь читателям, которые хотят чего-то похожего, initializer_listно не подпадают под все ограничения, которые делают это полезным. :)
underscore_d

@underscore_d какое из ограничений я упустил из виду?
Ричард Ходжес

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

0

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

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

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


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

0

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

Класс-оболочка предназначен для использования в том виде, в котором 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)
}

0

Вместо использования a 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


-1

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


4
Зачем вам определять категорию значений во время выполнения, если компилятор уже знает это?
fredoverflow

1
Пожалуйста, прочтите блог и оставьте мне комментарий, если вы не согласны или у вас есть лучшая альтернатива. Даже если компилятору известна категория значений, initializer_list не сохраняет ее, поскольку имеет только константные итераторы. Таким образом, вам нужно «захватить» категорию значений при создании initializer_list и передать ее, чтобы функция могла использовать ее по своему усмотрению.
Sumant

5
Этот ответ в основном бесполезен без перехода по ссылке, а ответы SO должны быть полезны без перехода по ссылкам.
Yakk - Adam Nevraumont

1
@Sumant [копирование моего комментария из идентичного поста в другом месте] Действительно ли этот огромный беспорядок дает какие-либо измеримые преимущества для производительности или использования памяти, и если да, то достаточно большое количество таких преимуществ, чтобы адекватно компенсировать то, как ужасно он выглядит, и тот факт, что он требуется около часа, чтобы понять, что он пытается сделать? Я в этом немного сомневаюсь.
underscore_d
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.