Force a compile time error if std::move will result in an unintended copy?

本小妞迷上赌 提交于 2019-11-30 11:27:16

问题


In his GoingNative 2013 talk, Scott Meyers pointed out that std::move is no guarantee that the generated code will actually perform a move.

Example:

void foo(std::string x, const std::string y) {
  std::string x2 = std::move(x); // OK, will be moved
  std::string y2 = std::move(y); // compiles, but will be copied
}

Here, the move constructor cannot be applied but because of overload resolution, the normal copy constructor will be used instead. This fallback option may be crucial for backward compatibility with C++98 code, but in the example above it is most likely not what the programmer intended.

Is there a way to enforce that a move constructor will be called?

For example, assume that you want to move a huge matrix. If your application really depend on the Matrix to be moved, it would be great to immediately get a compile error if a move is not possible. (Otherwise, you the performance problem may slip easily through unit tests and you will only find out after some profiling.)

Lets call this guaranteed move strict_move. I would like to be able to write code like this:

void bar(Matrix x, const Matrix y) {
  Matrix x2 = strict_move(x); // OK
  Matrix y2 = strict_move(y); // compile error
}

Is it possible?

Edit:

Thanks for the great answers! There were some legitimate requests to clarify my question:

  • Should strict_move fail if the input is const?
  • Should strict_move fail if the result will not lead to an actual move operation (even though the copy might be as fast as a move, e.g., const complex<double>)?
  • Both?

My original idea was very vague: I considered Scott Meyers examples quite alarming, so I wondered if it is possible to have the compiler prevent such unintended copies.

Scott Meyers mentioned in his talk that a general compiler warning is not an option as it would result in a huge number a false positives. Instead I want to communicate to the compiler something like "I'm 100% sure that this must always resulting in a move operation and a copy is too expensive for this specific type".

Thus, I would have offhandedly said that strict_move should fail in both cases. Meanwhile I'm not sure what would be best. Another aspects that I didn't consider is noexcept.

From my side, the exact semantics of strict_move are open. Everything that helps to prevent some dumb mistakes at compile time without having serious drawbacks is fine.


回答1:


I advise against writing a general strict_move that is detecting const. I think that is not really what you're looking for. Do you want this to flag a const complex<double>, or a const pair<int, int>? These types will copy as fast they move. Flagging them would just be an irritant.

If you want to do this, I recommend instead checking to see if the type is noexcept MoveConstructible. This will work perfectly for std::string. If the copy constructor of string is accidentally called, it is not noexcept, and therefore will be flagged. But if the copy constructor of pair<int, int> is accidentally called, do you really care?

Here is a sketch of what this would look like:

#include <utility>
#include <type_traits>

template <class T>
typename std::remove_reference<T>::type&&
noexcept_move(T&& t)
{
    typedef typename std::remove_reference<T>::type Tr;
    static_assert(std::is_nothrow_move_constructible<Tr>::value,
                  "noexcept_move requires T to be noexcept move constructible");
    static_assert(std::is_nothrow_move_assignable<Tr>::value,
                  "noexcept_move requires T to be noexcept move assignable");
    return std::move(t);
}

I decided to check against is_nothrow_move_assignable as well, as you don't know whether the client is constructing or assigning the lhs.

I opted for internal static_assert instead of an external enable_if because I don't expect noexcept_move to be overloaded, and the static_assert will yield a clearer error message when triggered.




回答2:


First I would like to state that you previous answers will not solve your problem completely.

Counterexample where previous solutions (@Kerrerk and @0x499602D2) fail: assume that you wrote your matrix class with a move constructor that throws an exception. Now assume that you want to move std::vector<matrix>. This article shows that you can't have the "strong exception guarantee" if the matrix class elements that the std::vector<matrix> holds were actually moved (what happen if the jth element move constructor throws an exception ? You would lose data because there is no way to recover the elements you have already moved!).

That is why stl containers implement .push_back(), .reserve() and their move constructor using std::move_if_noexcept to move the elements they hold. Here is an example implementation of reserve() taken from open-std :

void reserve(size_type n)
{
    if (n > this->capacity())
    {
        pointer new_begin = this->allocate( n );
        size_type s = this->size(), i = 0;
        try
        {
            for (;i < s; ++i)
                 new ((void*)(new_begin + i)) value_type( std::move_if_noexcept( (*this)[i]) ) );
        }
        catch(...)
        {
            while (i > 0)                 // clean up new elements
               (new_begin + --i)->~value_type();

            this->deallocate( new_begin );    // release storage
            throw;
        }
        // -------- irreversible mutation starts here -----------
        this->deallocate( this->begin_ );
        this->begin_ = new_begin;
        this->end_ = new_begin + s;
        this->cap_ = new_begin + n;
    }
}

So, if you don't enforce that your move and default constructor is noexcept, you won't guarantee that functions like std::vector.resize() or std::move (for stl containers) will never copy the matrix class and that is the correct behavior (otherwise you may lose data)




回答3:


You can just make your own version of move that doesn't allow constant return types. For example:

#include <utility>
#include <string>
#include <type_traits>


template <typename T>
struct enforce_nonconst
{
    static_assert(!std::is_const<T>::value, "Trying an impossible move");
    typedef typename std::enable_if<!std::is_const<T>::value, T>::type type;
};

template <typename T>
constexpr typename enforce_nonconst<typename std::remove_reference<T>::type>::type &&
mymove(T && t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type &&>(t);
}

void foo(std::string a, std::string const b)
{
    std::string x = std::move(a);
    std::string y = std::move(b);
}

void bar(std::string a, std::string const b)
{
    std::string x = mymove(a);
    // std::string y = mymove(b);  // Error
}

int main() { }


来源:https://stackoverflow.com/questions/18646874/force-a-compile-time-error-if-stdmove-will-result-in-an-unintended-copy

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!