Принудите ошибку времени компиляции, если std:: move приведет к непреднамеренной копии?

В своем разговоре GoingNative 2013 Скотт Мейерс отметил, что std::move не гарантирует, что сгенерированный код действительно выполнит ход.

Пример:

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
}

Здесь конструктор перемещения не может быть применен, но из-за разрешения перегрузки вместо него будет использоваться обычный конструктор копирования. Этот резервный вариант может иметь решающее значение для обратной совместимости с кодом С++ 98, но в приведенном выше примере это скорее всего не то, что предполагал программист.

Есть ли способ принудительно заставить конструктор перемещения вызываться?

Например, предположим, что вы хотите переместить огромную матрицу. Если ваше приложение действительно зависит от перемещаемой матрицы, было бы замечательно сразу же получить ошибку компиляции, если переход невозможен. (В противном случае проблема производительности может легко проскальзывать через модульные тесты, и вы узнаете только после некоторого профилирования.)

Позволяет выполнить этот гарантированный ход strict_move. Я хотел бы иметь возможность писать код следующим образом:

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

Возможно ли это?

Edit:

Спасибо за отличные ответы! Были некоторые законные просьбы, чтобы уточнить мой вопрос:

  • Должен ли strict_move сбой, если вход const?
  • Если strict_move завершится сбой, если результат не приведет к фактической операции перемещения (даже если копия может быть такой же быстрой, как и перемещение, например, const complex<double>)?
  • Как?

Моя оригинальная идея была очень расплывчатой: я считал примеры Скотта Мейерса довольно тревожными, поэтому я подумал, возможно ли компилятор предотвратить такие непреднамеренные копии.

Скотт Мейерс упомянул в своем разговоре, что предупреждение общего компилятора не является вариантом, так как это приведет к тому, что огромное количество будет ложным срабатыванием. Вместо этого я хочу сообщить компилятору что-то вроде "Я на 100% уверен, что это должно всегда приводить к операции перемещения, а копия слишком дорогая для этого конкретного типа".

Таким образом, я бы беззастенчиво сказал, что strict_move должен завершиться неудачей в обоих случаях. Между тем я не уверен, что было бы лучше. Другими аспектами, которые я не рассматривал, является noexcept.

С моей стороны точная семантика strict_move открыта. Все, что помогает предотвратить некоторые немые ошибки во время компиляции без серьезных недостатков, - это хорошо.

Ответ 1

Я рекомендую написать общий strict_move, который обнаруживает const. Я думаю, что это не то, что вы ищете. Вы хотите, чтобы это обозначало const complex<double>, или const pair<int, int>? Эти типы будут копироваться так быстро, как только они перемещаются. Пометить их было бы просто раздражителем.

Если вы хотите это сделать, я рекомендую вместо этого проверить, есть ли тип noexcept MoveConstructible. Это отлично работает для std::string. Если конструктор копирования string случайно вызван, это не исключение, и поэтому будет помечено. Но если конструктор копирования pair<int, int> случайно вызван, вам действительно все равно?

Вот эскиз того, как это будет выглядеть:

#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);
}

Я решил проверить и на is_nothrow_move_assignable, так как вы не знаете, создает ли клиент или назначает lhs.

Я выбрал внутренний static_assert вместо внешнего enable_if, потому что я не ожидаю, что noexcept_move будет перегружен, а static_assert даст более четкое сообщение об ошибке при запуске.

Ответ 2

Вы можете просто создать свою собственную версию move, которая не допускает постоянные типы возвращаемых данных. Например:

#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() { }

Ответ 3

Сначала я хотел бы сказать, что предыдущие ответы не полностью разрешают вашу проблему.

Контрпример, где предыдущие решения (@Kerrerk и @0x499602D2) не работают: предположим, что вы написали свой матричный класс с конструктором перемещения, который генерирует исключение. Теперь предположим, что вы хотите переместить std::vector<matrix>. В этой статье показано, что вы не можете иметь "сильную гарантию исключения", если элементы класса матрицы, которые были сохранены в std::vector<matrix>, были фактически перемещены (что произойдет, если j-й элемент move constructor генерирует исключение? Вы потеряете данные, потому что нет способа восстановить уже перемещенные элементы!).

Вот почему stl-контейнеры реализуют .push_back(), .reserve() и их конструктор перемещения, используя std::move_if_noexcept для перемещения элементов, которые они хранят. Вот пример реализации reserve(), взятый из 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;
    }
}

Итак, если вы не применяете к тому, что ваш конструктор move и default не является исключением, вы не будете гарантировать, что такие функции, как std::vector.resize() или std:: move (для stl-контейнеров) никогда не будут копировать матрицу класс, и это правильное поведение (в противном случае вы можете потерять данные)