Любой риск перемещения элементов 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