Initializer_list та перемістити семантику


98

Чи дозволяється мені переміщувати елементи з a 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 ++, я волію бути в безпеці, ніж шкодувати і запитувати.


Основна мова визначає, що об'єкт, на який посилається an initializer_list<T>, не є const. Мовляв, initializer_list<int>відноситься до intоб'єктів. Але я думаю, що це дефект - передбачається, що компілятори можуть статично розподіляти список у пам'яті лише для читання.
Йоханнес Шауб - літб,

Відповіді:


90

Ні, це не буде працювати за призначенням; ви все одно отримаєте копії. Я дуже здивований цим, оскільки я думав, що initializer_listіснувало, щоб утримувати безліч тимчасових магазинів, поки їх не було move.

beginа endдля initializer_listповернення const T *, тож результат moveу вашому коді T const &&- незмінне посилання rvalue. З такого виразу не можна змістовно рухатись. Він буде прив’язаний до параметра функції типу, T const &оскільки rvalues ​​дійсно прив’язуються до посилань 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 Його не можна зламати. Я не бачу, що саме цей код повинен виконувати wrt 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посилається на статичну пам'ять. Ось для чого клас. Ви не можете рухатися зі статичної пам'яті, оскільки рух передбачає її зміну. Ви можете лише скопіювати з нього.


Const xvalue все ще є xvalue і initializer_listпосилається на стек, якщо це необхідно. (Якщо вміст не є постійним, він все ще безпечний для потоків.)
Potatoswatter

5
@Potatoswatter: Ви не можете рухатися від постійного об'єкта. Сам initializer_listоб'єкт може бути xvalue, але це вміст (фактичний масив значень, на які він вказує) const, оскільки цей вміст може бути статичним значенням. Ви просто не можете перейти від вмісту initializer_list.
Nicol Bolas,

Дивіться мою відповідь та її обговорення. Він перемістив ітератор, який не має посилання, constстворивши xvalue. moveможе бути безглуздим, але це законно і навіть можливо оголосити параметр, який приймає саме це. Якщо переміщення певного типу виявляється забороною, це може навіть працювати правильно.
Potatoswatter

1
@Potatoswatter: Стандарт C ++ 11 витрачає багато мови, гарантуючи, що не тимчасові об'єкти насправді не переміщуються, якщо ви ними не користуєтесь std::move. Це гарантує, що ви можете перевірити, коли відбувається операція переміщення, оскільки це впливає як на джерело, так і на пункт призначення (ви не хочете, щоб це відбувалося неявно для іменованих об’єктів). Через це, якщо ви використовуєте std::moveв місці, де операція переміщення не відбувається (і фактичного переміщення не відбудеться, якщо у вас є constxvalue), тоді код вводить в оману. Я вважаю, що помилкою є те, що std::moveможна викликати constоб’єкт.
Nicol Bolas,

1
Можливо, але я все-таки візьму менше винятків із правил щодо можливості введення коду в оману. У будь-якому випадку, саме тому я відповів "ні", хоча це і законно, і результат є xvalue, навіть якщо він буде прив'язуватися лише як const lvalue. Чесно кажучи, у мене вже був короткий флірт із const &&зібраним сміттям класом з керованими вказівниками, де все, що було релевантним, змінювалося, і переміщення переміщувало управління вказівниками, але це не впливало на значення, що міститься. Завжди є хитрі випадки краю: v).
Potatoswatter

2

Це не буде працювати, як зазначено, оскільки list.begin()має типconst T * , і ви не можете перейти від постійного об'єкта. Мовні дизайнери, напевно, зробили це для того, щоб дозволити списки ініціалізаторів містити, наприклад, константи рядків, з яких було б недоречно переходити.

Однак, якщо ви потрапили в ситуацію, коли вам відомо, що список ініціалізатора містить вирази rvalue (або ви хочете змусити користувача писати їх), тоді є хитрість, яка змусить його працювати (мене надихнула відповідь Sumant для це, але рішення набагато простіше, ніж це). Вам потрібно, щоб елементи, що зберігаються в списку ініціалізатора, були не 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;
}

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


Рядові константи? Подобається "Hello world"? Якщо ви рухаєтесь від них, ви просто копіюєте вказівник (або прив'язуєте посилання).
dyp

1
"На одне питання потрібна відповідь" Ініціалізатори всередині {..}прив’язані до посилань у параметрі функції rref_capture. Це не продовжує їх життя, вони все ще знищуються в кінці повного виразу, в якому вони були створені.
dyp

Відповідно до коментаря TC від іншої відповіді: Якщо у вас є кілька перевантажень конструктора, оберніть std::initializer_list<rref_capture<T>>якусь обрану вами функцію перетворення, скажімо, std::decay_t- щоб заблокувати небажані відрахування.
Невислаблена Моніка,

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 ви абсолютно праві. Я вважаю, що обмін знаннями, пов’язаними з цим питанням, сам по собі хороший. У цьому випадку, можливо, це допомогло ОП, а можливо, ні - він не відповів. Однак найчастіше ОП та інші вітають додаткові матеріали, що стосуються цього питання.
Річард Ходжес,

Звичайно, це дійсно може допомогти читачам, які хочуть щось подібне, 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;
}

Варіадичні шаблони можуть обробляти посилання на значення 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, є посиланням const.


4
Чому б ви хотіли визначити категорію значень під час виконання, коли компілятор це вже знає?
fredoverflow

1
Будь ласка, прочитайте блог і залиште мені коментар, якщо ви не згодні чи маєте кращу альтернативу. Навіть якщо компілятор знає категорію значень, Initializer_list не зберігає її, оскільки вона має лише ітератори const. Отже, вам потрібно "захопити" категорію значень, коли ви створюєте список ініціалізаторів і передаєте його, щоб функція могла використовувати його як завгодно.
Sumant

5
Ця відповідь в основному марна, не переходячи за посиланням, і відповіді SO мають бути корисними без переходу за посиланнями.
Якк - Адам Неврамон

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