This question builds on this @FredOverflow's question.
CLARIFICATION:
initializer_list
approach is required as the VC++2012 has a bug the prevents forwarded expansion of namespaced arguments._MSC_VER <= 1700
has the bug.
I've written a variadic template function that collapses any number of arguments in a typed container. I use the type's constructor to convert the variadic arguments into consumable values. E.g. _variant_t
:)
I need this for my MySql
C++ library when pushing arguments to prepared statements in one strike, while my MySqlVariant
converts the input data to MYSQL_BIND
s. As I may work with BLOB
s, I'd like to avoid copy-construct as much as possible when I can move&&
the large containers around.
I've done a simple test and noticed that the initialize_list
does copy-construct
for the stored elements and destroys them when it goes out of scope. Perfect... Then I tried to move the data out of the initializer_list
and, to my surprise, it used lvalues
not rvalues
as I expected with std::move
.
Funny as this happens just after Going Native 2013 clearly warned me that move does not move, forward does not forward... be like water, my friend - to stay on the deep end of thinking.
But that did not stop me :) I decided to const_cast
the initializer_list
values and still move them out. An eviction order needs to be enforced. And this is my implementation:
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;
}
Using it is easy:
// 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 :)
I've also uploaded a full test on IDEone ^ that shows as the memory of a std::string
moves properly with this function. (I would paste it all here but it's slightly long...)
As long as the _variant_t
(or whatever final wrapping object) has the right constructors, it's great. And if the data can be moved out, it's even better. And it pretty much works as I tested it and things std::move
in the right direction :)
My questions are simple:
std::move
does not work by default on initializer_list
, is what I'm doing here: illegal, immoral, hacky... or just plain wrong?PS: I'm a self-taught Windows Native C++
developer, ignorant of the standards.
^ my excuse if I'm doing really non-standard things here.
UPDATE
Thanks everyone, I have both the answer and the solution (a short and long one) now.
And I love the C++11 side of SO. Many knowledgeable people here...
In the general case, this is undefined behavior, unfortunately. At §8.5.4/5, emphasis mine:
An object of type
std::initializer_list<E>
is constructed from an initializer list as if the implementation allocated a temporary array ofN
elements of typeconst E
, whereN
is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and thestd::initializer_list<E>
object is constructed to refer to that array.
Where you see a std::initializer_list<E>
, you can act as if it's a const E[N]
.
So when you const_cast
away the const
, you're looking at a mutable reference to a const
object. Any modification to a const
object is undefined behavior.
When you move that std::string
, you're modifying a const
object. Unfortunately , one of the behaviors of undefined behavior is seemingly correct behavior. But this is technically undefined.
Note that when you std::move(int)
into another, that is well-defined because int
's can only be copied, so the move does nothing and no const
objects are modified. But in general, it's undefined.
You can reduce the specializations by one. This "universal reference" specialization should also cover the lvalue reference, in which case std::move
will do nothing.
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;
}
Source: Scott Meyers talk at GoingNative2013; finely detailed in this accu article
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With