Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any risk to moving const_cast elements out of a std::initializer_list?

Tags:

c++

c++11

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_BINDs. As I may work with BLOBs, 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:

  • Am I doing this right standard-wise?
  • Is the fact that it's working right intended or just a side effect?
  • If 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...

like image 395
CodeAngry Avatar asked Sep 08 '13 18:09

CodeAngry


2 Answers

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 of N elements of type const E, where N 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 the std::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.

like image 61
GManNickG Avatar answered Oct 02 '22 14:10

GManNickG


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

like image 28
Senti Bachcha Avatar answered Oct 02 '22 15:10

Senti Bachcha