LINUX.ORG.RU

Как гарантировать move операцию без copy операций (is_true_move_constructible)?

 , , ,


0

2

Столкнулся недавно с ситуацией, что класс делал копирование вместо перемещения, т.к. одним из полей класса было не перемещаемое поле, что-то вроде такого:

UPD: пример рабочий, оставлено для истории. Демонстрируется в обсуждениях.


struct __attribute__((packed, aligned(1))) Token {
   char abc = 0;
};

struct Data {
   char *allocatedData = nullptr;

   Data(Data &&o) : allocatedData(std::exchange(o.allocatedData, nullptr) {}
   Data &operator=(Data &&o) { swap(allocatedData, o.allocatedData); }
   // плюс еще конструкторы/деструкторы и присвоение
   //  которые реально выделяют память
};
...

// в таком pair этот Data перестает быть перемещаемым, только копируемым.
// и в результате при вставках в список для каждого сдвигаемого
// элемента происходит копирование, а не перемещение.
MyList<pair<Token,Data>> listSortedByToken;

При дальнейшем расследовании оказалось, что std::is_move_constructible && std::is_move_assignable сообщают true на такой pair, т.к. оно по прежнему содержит конструктор копирования. Да и на сам Token так же сообщают true.

Вопрос, можно ли как-либо идентифицировать, что что объект будет перемещаться, а не копироваться при использовании move/swap? Что бы можно было поставить на это static_assert.

UPD:

В общем, данный пример оказался нормально рабочим, а у меня раннее не работало из-за запутанности объявлений с =default && =delete у присвоений и конструкторов.

Но вопрос остается, можно ли как гарантировать, что объект будет перемещаться, а не копироваться? При том, что у объекта должны быть и конструкторы копирования. Что бы выполнение std::move строго вызывало конструкцию Data(Data&&), независимо от того, что move применен не к самому Data, а скажем к классу который инкапсулирует Data где-нибудь. Т.е. теперь это вопрос к классу Data, что бы он мог инкапсулироваться только в те классы, которые позволят ему быть перемещаемым.

=============================

UPD: В общем нашелся такой вот метод:


template <typename T, bool P>
struct is_movecopy_helper;

template <typename T>
struct is_movecopy_helper<T, false> {
    typedef T type;
};

template <typename T>
struct is_movecopy_helper<T, true> {
    template <typename U>
    struct Dummy : public U {
        Dummy(const Dummy&) = delete;
        Dummy(Dummy&&) = default;
    };
    typedef Dummy<T> type;
};

template <class T>
struct has_move_constructor
: std::integral_constant<bool,
    std::is_move_constructible<
       typename is_movecopy_helper<T, std::is_class<T>::value>::type
    >::value> { };
   static_assert(has_move_constructor<T>::value);

Вроде работате, взят отсюда и немного подкорректирован: https://stackoverflow.com/questions/7054952/type-trait-for-moveable-types



Последнее исправление: victor79 (всего исправлений: 3)

Удалить copy конструктор и copy assignment operator.

Примерно так:

struct Data {
   char *allocatedData = nullptr;

   Data(Data &&o) : allocatedData(std::exchange(o.allocatedData, nullptr) {}

   Data(const Data &o) = delete
   Data(Data &o) = delete


   Data &operator=(Data &&o) { swap(allocatedData, o.allocatedData); }

   Data &operator=(const Data &o) = delete; 
   Data &operator=(Data &o) = delete;
};
Kroz ★★★★★
()
Последнее исправление: Kroz (всего исправлений: 2)
Ответ на: комментарий от Kroz

И тогда он не будет копироваться:

Data a("myData");
Data b(a); // error
Data c = a; // error

Но вопрос не про конкретно Data, а про класс-контейнер для которого Data является параметром шаблона - что бы он мог проверить, что в него положили правильный класс, которым он может таким образом управлять.

victor79
() автор топика

Странно ванговать по какому-то самописному MyList (сам ведь писал, откуда нам знать что там за ливер внутри). Ну а что касается std: move не используется, когда ctr/= не сделаны noexcept. Из-за этого после выброса исключения в середине мов оператора нет гарантий, что объект, с которого ты перемещал, в валидном состоянии, поэтому выполняется копирование (ну аргументы примерно такие у этого поведения).

pavlick ★★
()
Ответ на: комментарий от pavlick

std: move не используется, когда ctr/= не сделаны noexcept

Что за ctr/= не понятно, но в программе ислючений не происходит. Все работает корректно, только дольше чем ожидалось. И я долго разбирался, что такого поменялось, что вдруг в данном случае работает дольше ожидаемого. А тот packed был глубоко закопан из древних времен, я про него и не помнил уже.

Я думаю vector поведет себя так же. В MyList используется принцип упомянутый здесь: Будет ли корректно не дестроить объект в случае перемещений в массиве?

Т.е. упрощенно:

using T = pair<Token,Data>;
T a{...};
T b = std::move(a); // здесь выполнит копирование, а не перемещение

victor79
() автор топика
Последнее исправление: victor79 (всего исправлений: 1)
Ответ на: комментарий от victor79

Что за ctr/= не понятно

move coustructor/move operator=.

но в программе ислючений не происходит.

Не важно, когда выбирается перегрузка для какого-нибудь push_back контейнера то компилятор видит, что в value_type =/конструктор не noexcept и перемещающие версии отключаются через SFINAE. Цитата с вектора:

If T's move constructor is not noexcept and T is not CopyInsertable into *this, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified. 

Я хз что там за вектор у тебя такой, может это:

template <typename T>
using MyList<T> = std::vector<T>
pavlick ★★
()
Ответ на: комментарий от victor79

Делать static_assert на кастомную is_movable проверку, которую специализировать для std::pair и рекурсивно проверять pair::first_type и pair::second_type?

xaizek ★★★★★
()

Хм. Сейчас перепроверил, ошибки в описанном случае нет, и перемещение выполняется в желаемом виде:

    struct __attribute__((packed, aligned(1))) Token {
        char a = 0;
        Token() {}
        Token(int _a) : a(_a) {}
    };

    struct Data {
        int b = 0;
        Data(int _b) : b(_b) { qDebug() << "Data(int)"; }
        Data() { qDebug() << "Data()"; }
        Data(const Data&) { qDebug() << "Data(const Data&)"; }
        Data(Data&&) { qDebug() << "Data(Data&&)"; }
        Data &operator=(const Data&) { qDebug() << "=const Data&"; return *this; }
        Data &operator=(Data &&) { qDebug() << "=Data&&"; return *this; }
    };

    using T = pair<Token,Data>;

    T a{0,0};
    T b(a);

    qDebug() << "===testMove1";
    T c = std::move(a);

    qDebug() << "===testMove2";
    T d = T(Token(5),Data(6));

    qDebug() << "===withList";

    MyList<T> list;
    list += a;
    qDebug() << "===testListInsert";
    list.insert(list.begin(), b);
    qDebug() << "===END";
Data(int)
Data(const Data&)
===testMove1
Data(Data&&)
===testMove2
Data(int)
Data(Data&&)
===withList
Data(const Data&)
===testListInsert
Data(Data&&)
Data(Data&&)
Data(const Data&)
===END

Получается, что ошибка была не в packed, а в том, что я намудрил при указании в конструкторах и в перемещениях с атрибутами =default или =delete. Там кучка нюансов на это.

В общем, вопрос в части использования именно packed закрыт. Но существует возможность указания таких конструкторов, при совмещении которых в инкапсуляции с другими объектами, вроде нормальных, вторые становятся не нормальными.

Возможно is_move_constructible && is_move_assignable такие ситуации ловит, но мне теперь уже не проверить.

victor79
() автор топика
Последнее исправление: victor79 (всего исправлений: 2)
Ответ на: комментарий от victor79

Похоже на разговор с самим собой. Как объявлен MyList, typedef это или нет - осталось загадкой. Ну да ладно. А гарантировать нужно две вещи по большому счету - их наличие + атрибут noexcept на конструкторе перемещающем и операторе =, это в случаях, когда используются стд контейнеры, у велосипедистов свое кино. Проверить оба этих условия статик ассертом можно так:

static_assert(  noexcept(S(std::declval<S>())) );
static_assert(  noexcept(std::declval<S>() = std::declval<S>()) );

pavlick ★★
()
Ответ на: комментарий от pavlick

Похоже на разговор с самим собой

Это я посмотрел, спасибо. В частности, стандартные контейнеры используют move_if_noexcept, что использует move если он noexcept, иначе использует copy, из-за чего и возникает такое поведение.

У меня своя реализация списка, почему так это долго доказывать. Там я использовал чисто move, поэтому описанного тобой случая не происходило.

victor79
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.