Есть какой-нибудь риск перемещения элементов const_cast из списка std::initializer_list?
Этот вопрос основан на вопросе этого @FredOverflow.
РАЗЪЯСНЕНИЕ:
initializer_list
подход необходим, так как в VC++2012 есть ошибка, препятствующая перенаправленному расширению аргументов пространства имен._MSC_VER <= 1700
есть ошибка
Я написал переменную шаблонную функцию, которая объединяет любое количество аргументов в типизированном контейнере. Я использую конструктор типа, чтобы преобразовать переменные аргументы в потребляемые значения. Например _variant_t
:)
Мне нужно это для моего MySql
C++ библиотека при нажатии аргументов для подготовленных операторов в один удар, в то время как мой MySqlVariant
преобразует входные данные в MYSQL_BIND
s. Как я могу работать с BLOB
s, я бы хотел как можно больше избегать копирования, когда смогу move&&
большие контейнеры вокруг.
Я сделал простой тест и заметил, что initialize_list
делает copy-construct
для хранимых элементов и уничтожает их, когда он выходит из области видимости. Отлично... Тогда я попытался переместить данные из initializer_list
и, к моему удивлению, он использовал lvalues
не 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's 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++
разработчик, неосведомленный о стандартах.
^ мое оправдание, если я делаю действительно нестандартные вещи здесь.
ОБНОВИТЬ
Спасибо всем, у меня есть и ответ, и решение (короткое и длинное).
И я люблю сторону C++11 SO. Здесь много знающих людей...
3 ответа
В общем случае это неопределенное поведение, к сожалению. В §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
объект неопределенного поведения.
Когда вы двигаете это std::string
модифицируешь const
объект. К сожалению, одним из поведений неопределенного поведения является, казалось бы, правильное поведение. Но это технически не определено.
Обратите внимание, что когда вы std::move(int)
в другое, это четко определено, потому что int
можно только скопировать, поэтому ход ничего не делает и нет const
объекты изменены. Но в целом это не определено.
Вы можете уменьшить специализации на одну. Эта специализация "универсальная ссылка" должна также охватывать ссылку 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; подробно описано в этой статье
Нашел альтернативное решение для тех, кто разделяет мою боль:
#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: VC++ 2012 CTPnov2012 имеет ошибку, которая не позволяет работать с классами пространства имен. Итак, первоначальное решение без const_cast
должен сделать. Весь мой код является пространством имен. В VC2013 это исправлено теоретически... поэтому я буду менять код при обновлении.