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


person fredoverflow    schedule 19.11.2011    source источник
comment
Базовый язык определяет, что объекты, на которые указывает initializer_list<T>, являются неконстантными. Например, initializer_list<int> относится к int объектам. Но я думаю, что это дефект - предполагается, что компиляторы могут статически размещать список в памяти только для чтения.   -  person Johannes Schaub - litb    schedule 19.11.2011


Ответы (8)


Нет, это не сработает так, как задумано; вы все равно получите копии. Я очень удивлен этим, так как думал, что 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 поддержке типов, предназначенных только для перемещения. Это только первый черновик, и он еще нигде не реализован, но вы можете увидеть его для более подробного анализа проблемы.

person Potatoswatter    schedule 19.11.2011
comment
В случае, если это неясно, это все равно означает, что использование std::move безопасно, если не продуктивно. (За исключением T const&& конструкторов перемещения.) - person Luc Danton; 19.11.2011
comment
Я не думаю, что вы могли бы привести весь аргумент либо const std::initializer_list<T>, либо просто std::initializer_list<T> таким образом, чтобы не вызывать слишком часто сюрпризов. Учтите, что каждый аргумент в initializer_list может быть либо const, либо нет, и это известно в контексте вызывающего, но компилятор должен сгенерировать только одну версию кода в контексте вызываемого (т.е. внутри foo он ничего не знает об аргументах, которые передает вызывающий) - person David Rodríguez - dribeas; 19.11.2011
comment
@David: Хороший замечание, но все же было бы полезно, чтобы std::initializer_list && перегрузка что-то делала, даже если также требуется перегрузка, не являющаяся ссылкой. Полагаю, это было бы еще более запутанным, чем нынешняя ситуация, которая и так плоха. - person Potatoswatter; 19.11.2011
comment
Возможно, вас заинтересует эта статья: cpptruths. blogspot.fr/2013/10/ - person Jean-Bernard Jansen; 10.03.2014
comment
@JBJansen Его нельзя взломать. Я не понимаю, что именно этот код должен выполнять в отношении initializer_list, но как пользователь у вас нет необходимых разрешений для перехода от него. Безопасный код этого не сделает. - person Potatoswatter; 10.03.2014
comment
Да, вы можете взломать его, правильно используя изменяемые значения. Такой взлом может быть опасен, как и forward_as_tuple, при неправильном использовании. Возможно, вас это заинтересует: stackoverflow.com/questions/13957166/ - person Jean-Bernard Jansen; 23.03.2014
comment
@Potatoswatter, поздний комментарий, но каков статус предложения. Есть ли шанс, что он попадет в C ++ 20? - person WhiZTiM; 19.07.2017
comment
@WhiZTiM В последнее время я не так активен в C ++ (ISO или иначе), но все возможно. Это открытый стандарт - при желании вы можете продвигать идею сами! C ++ 20, безусловно, должен быть открыт для новых идей, поскольку C ++ 17 еще не ратифицирован. - person Potatoswatter; 25.07.2017
comment
Есть ли прогресс в этом предложении? Я также довольно удивлен, что инициализатор перечисляет принудительные копии. - person Michaël; 25.02.2021

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

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

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

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

person Nicol Bolas    schedule 19.11.2011
comment
Константа xvalue по-прежнему является xvalue, и initializer_list ссылается на стек, если это необходимо. (Если содержимое не является постоянным, оно по-прежнему является потокобезопасным.) - person Potatoswatter; 20.11.2011
comment
@Potatoswatter: вы не можете двигаться от постоянного объекта. Сам объект initializer_list может быть значением x, но его содержимое (фактический массив значений, на которые он указывает) - это const, потому что это содержимое может быть статическими значениями. Вы просто не можете перейти от содержимого initializer_list. - person Nicol Bolas; 20.11.2011
comment
Смотрите мой ответ и его обсуждение. Он переместил разыменованный итератор, получив const xvalue. move может быть бессмысленным, но это законно и даже возможно объявить параметр, который принимает именно это. Если перемещение определенного типа невозможно, оно может даже работать правильно. - person Potatoswatter; 20.11.2011
comment
@Potatoswatter: Стандарт C ++ 11 требует большого количества языков, гарантируя, что невременные объекты фактически не перемещаются, если вы не используете std::move. Это гарантирует, что при проверке вы сможете определить, когда происходит операция перемещения, поскольку она влияет как на источник, так и на место назначения (вы не хотите, чтобы это происходило неявно для именованных объектов). Из-за этого, если вы используете std::move в месте, где операция перемещения не выполняется (и никакого фактического перемещения не произойдет, если у вас есть const xvalue), тогда код вводит в заблуждение. Я считаю ошибкой возможность вызова std::move объекта const. - person Nicol Bolas; 20.11.2011
comment
Возможно, но я все равно буду делать меньше исключений из правил из-за возможности введения в заблуждение кода. В любом случае, именно поэтому я ответил «нет», хотя это законно, и результатом будет xvalue, даже если он будет связываться только как const lvalue. Честно говоря, у меня уже было краткое заигрывание с const && в классе со сборкой мусора с управляемыми указателями, где все относящееся к делу было изменяемым, а перемещение перемещало управление указателем, но не влияло на содержащееся значение. Всегда есть сложные крайние случаи: v). - person Potatoswatter; 20.11.2011

Это не будет работать, как указано, потому что 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.

person Marc van Leeuwen    schedule 07.07.2014
comment
Строковые константы? Нравится "Hello world"? Если вы двигаетесь от них, вы просто копируете указатель (или привязываете ссылку). - person dyp; 07.07.2014
comment
Один вопрос требует ответа Инициализаторы внутри {..} привязаны к ссылкам в параметре функции rref_capture. Это не продлевает их срок службы, они все равно уничтожаются в конце полного выражения, в котором они были созданы. - person dyp; 07.07.2014
comment
Согласно комментарию TC из другого ответа: если у вас есть несколько перегрузок конструктора, заключите std::initializer_list<rref_capture<T>> в некоторые выбранное вами свойство трансформации - скажем, std::decay_t - для блокировки нежелательного вывода. - person Kuba hasn't forgotten Monica; 27.10.2016

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

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

#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));
}
person Richard Hodges    schedule 01.06.2017
comment
Вопрос был в том, можно ли переместить initializer_list, а не в том, есть ли у кого-нибудь обходные пути. Кроме того, основным преимуществом initializer_list является то, что он основан только на типе элемента, а не на количестве элементов, и, следовательно, не требует, чтобы получатели также были шаблонными - и это полностью теряет это. - person underscore_d; 02.07.2017
comment
@underscore_d, ты абсолютно прав. Я считаю, что делиться знаниями, связанными с этим вопросом, само по себе хорошо. В этом случае, возможно, это помогло OP, а возможно, нет - он не ответил. Однако чаще всего OP и другие приветствуют дополнительные материалы, связанные с вопросом. - person Richard Hodges; 03.07.2017
comment
Конечно, это действительно может помочь читателям, которые хотят что-то вроде initializer_list, но не подпадают под все ограничения, которые делают это полезным. :) - person underscore_d; 03.07.2017
comment
@underscore_d, какое из ограничений я упустил из виду? - person Richard Hodges; 03.07.2017
comment
Все, что я имею в виду, это то, что initializer_list (через магию компилятора) позволяет избежать использования шаблонных функций по количеству элементов, что по своей сути требуется для альтернатив, основанных на массивах и / или вариативных функциях, тем самым ограничивая диапазон случаев, когда последние могут использоваться. Насколько я понимаю, это как раз одно из основных оснований для использования initializer_list, поэтому, казалось, стоит упомянуть об этом. - person underscore_d; 03.07.2017

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

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

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

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

person Hiroshi Ichikawa    schedule 26.03.2017
comment
Вопрос был в том, можно ли переместить initializer_list, а не в том, есть ли у кого-нибудь обходные пути. Кроме того, основным преимуществом initializer_list является то, что он основан только на типе элемента, а не на количестве элементов, и, следовательно, не требует, чтобы получатели также были шаблонными - и это полностью теряет это. - person underscore_d; 02.07.2017

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

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

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

person Jorge Bellon    schedule 02.06.2020

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

person Sumant    schedule 18.09.2013
comment
Зачем вам определять категорию значения во время выполнения, если компилятор уже знает это? - person fredoverflow; 18.09.2013
comment
Пожалуйста, прочтите блог и оставьте мне комментарий, если вы не согласны или у вас есть лучшая альтернатива. Даже если компилятору известна категория значений, initializer_list не сохраняет ее, поскольку имеет только константные итераторы. Таким образом, вам нужно захватить категорию значений при создании initializer_list и передать ее, чтобы функция могла использовать ее по своему усмотрению. - person Sumant; 21.09.2013
comment
Этот ответ в основном бесполезен без перехода по ссылке, и ответы SO должны быть полезны без перехода по ссылкам. - person Yakk - Adam Nevraumont; 19.07.2016
comment
@Sumant [копирование моего комментария из идентичного поста в другом месте] Действительно ли этот огромный беспорядок дает какие-либо измеримые преимущества для производительности или использования памяти, и если да, то достаточно большое количество таких преимуществ, чтобы адекватно компенсировать то, как ужасно он выглядит, и тот факт, что он требуется около часа, чтобы понять, что он пытается сделать? Я в этом немного сомневаюсь. - person underscore_d; 09.08.2016