Любой риск перемещения элементов const_cast из std:: initializer_list?

Этот вопрос основывается на этом вопросе @FredOverflow.

ПОДТВЕРЖДЕНИЕ: initializer_list требуется, поскольку VС++ 2012 имеет ошибку, предотвращает пересылку расширение аргументов с именами. _MSC_VER <= 1700 имеет ошибку.

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

Мне нужно это для моей библиотеки MySql С++ при нажатии аргументов готовых операторов в один удар, а мой MySqlVariant преобразует входные данные в MYSQL_BIND s. Поскольку я могу работать с BLOB s, я бы хотел как можно больше избежать копирования-конструкции, когда могу move&& использовать большие контейнеры.

Я проделал простой тест и заметил, что initialize_list выполняет copy-construct для сохраненных элементов и уничтожает их, когда он выходит из области видимости. Perfect... Затем я попытался вывести данные из initializer_list, и, к моему удивлению, он использовал lvalues not rvalues, как я ожидал, с помощью std::move.

Забавно, как это происходит сразу после Going Native 2013 ясно предупредил меня, что перемещение не перемещается, вперед не вперед... быть как вода, мой друг - оставаться на глубоком конце мышления.

Но это меня не остановило:) Я решил const_cast значения initializer_list и по-прежнему их вытаскивать. Необходимо обеспечить соблюдение порядка выселения. И это моя реализация:

template <typename Output_t, typename ...Input_t>
inline Output_t& Compact(Output_t& aOutput, Input_t&& ...aInput){
    // should I do this? makes sense...
    if(!sizeof...(aInput)){
        return aOutput;
    }

    // I like typedefs as they shorten the code :)
    typedef Output_t::value_type Type_t;

    // can be either lvalues or rvalues in the initializer_list when it populated.
    std::initializer_list<Type_t> vInput = { std::forward<Input_t>(aInput)... };

    // now move the initializer_list into the vector.
    aOutput.reserve(aOutput.size() + vInput.size());
    for(auto vIter(vInput.begin()), vEnd(vInput.end()); vIter != vEnd; ++vIter){
        // move (don't copy) out the lvalue or rvalue out of the initializer_list.
        // aOutput.emplace_back(std::move(const_cast<Type_t&>(*vIter))); // <- BAD!
        // the answer points out that the above is undefined so, use the below
        aOutput.emplace_back(*vIter); // <- THIS is STANDARD LEGAL (copy ctor)!
    }

    // done! :)
    return aOutput;
}

Использовать его легко:

// You need to pre-declare the container as you could use a vector or a list...
// as long as .emplace_back is on duty!
std::vector<MySqlVariant> vParams;
Compact(vParams, 1, 1.5, 1.6F, "string", L"wstring",
    std::move(aBlob), aSystemTime); // MySql params :)

Я также загрузил полный тест на IDEone ^, который показывает поскольку память о std::string корректно перемещается с этой функцией. (Я бы вставлял все это здесь, но немного длиннее...)

Пока _variant_t (или какой-либо конечный оберточный объект) имеет правильные конструкторы, это здорово. И если данные могут быть перемещены, это еще лучше. И это в значительной степени работает, когда я тестировал его и вещи std::move в правильном направлении:)

Мои вопросы просты:

  • Я делаю это правильно стандартно?
  • Является ли тот факт, что он работает правильно, или просто побочный эффект?
  • Если std::move не работает по умолчанию на initializer_list, это то, что я здесь делаю: незаконный, аморальный, хакерский... или просто неправильно?

PS. Я разработчик Windows Native C++, не осведомленный о стандартах. ^ Извините, если я делаю действительно нестандартные вещи здесь.

UPDATE

Спасибо всем, у меня есть как ответ, так и решение (короткое и длинное).

И я люблю С++ 11 сторону SO. Многие знающие люди здесь...

Ответ 1

В общем случае это поведение undefined, к сожалению. В пункте §8.5.4/5 основное внимание уделяется:

Объект типа std::initializer_list<E> создается из списка инициализаторов , как если бы реализация выделяла временный массив элементов N типа const E, где N - это число элементов в списке инициализаторов. Каждый элемент этого массива инициализируется копией с соответствующим элементом списка инициализаторов, и объект std::initializer_list<E> создан для обращения к этому массиву.

Где вы видите std::initializer_list<E>, вы можете действовать так, как будто это const E[N].

Итак, когда вы const_cast прочь const, вы смотрите на изменяемую ссылку на объект const. Любая модификация объекта const - это поведение undefined.

Когда вы перемещаете этот std::string, вы изменяете объект const. К сожалению, одно из поведений поведения undefined - это, по-видимому, правильное поведение. Но это технически undefined.

Обратите внимание, что когда вы std::move(int) в другой, это хорошо определено, потому что int  может быть скопирована, поэтому движение ничего не делает, а объекты const не изменяются. Но в целом это undefined.

Ответ 2

Найденное альтернативное решение для всех, кто разделяет мою боль:

#if _MCS_VER <= 1700
// Use the code in the OP!
// VS 2012- stuff comes here.
#else
// VS 2013+ stuff comes here.
template <typename Output_t>
inline Output_t& Compact(Output_t& aOutput){
    return aOutput;
}

template <typename Output_t, typename First_t>
inline Output_t& Compact(Output_t& aOutput, const First_t& aFirst){
    aOutput.emplace_back(aFirst);
    return aOutput;
}

template <typename Output_t, typename First_t>
inline Output_t& Compact(Output_t& aOutput, First_t&& aFirst){
    aOutput.emplace_back(std::move(aFirst));
    return aOutput;
}

template <typename Output_t, typename First_t, typename ...Next_t>
inline Output_t& Compact(Output_t& aOutput, const First_t& aFirst, Next_t&& ...aNext){
    aOutput.emplace_back(aFirst);
    return Compact(aOutput, std::forward<Next_t>(aNext)...);
}

template <typename Output_t, typename First_t, typename ...Next_t>
inline Output_t& Compact(Output_t& aOutput, First_t&& aFirst, Next_t&& ...aNext){
    aOutput.emplace_back(std::move(aFirst));
    return Compact(aOutput, std::forward<Next_t>(aNext)...);
}
#endif // _MCS_VER <= 1700

PS: VС++ 2012 CTPnov2012 имеет BUG, ​​который мешает этому работать на пространствах с именами. Итак, начальное решение без const_cast должно выполняться. Весь мой код - это пространство имен. VC2013 имеет это фиксированное в теории... поэтому переключит код при обновлении.

Ответ 3

Вы можете уменьшить специализации на один. Эта "универсальная справочная" специализация должна также охватывать ссылку lvalue, и в этом случае std::move ничего не сделает.

template <typename Output_t, typename First_t>
inline Output_t& Compact(Output_t& aOutput, First_t&& aFirst){
    aOutput.emplace_back(std::forward<First_t>(aFirst));
    return aOutput;
}

Источник: Скотт Майерс беседует в GoingNative2013; подробно описанный в этой статье accu