Есть какой-нибудь риск перемещения элементов 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 это исправлено теоретически... поэтому я буду менять код при обновлении.

Другие вопросы по тегам