LINUX.ORG.RU

очередная путаница в голове про move

 ,


0

2

Начитавшись:

Я собственно попробовал и так и так:

#include <iostream>
#include <memory>
#include <utility>
using namespace std;

const int INCONSISTENT = 666;

struct A {
    int *p;

    A(): p(new int(42)) { cout << "A()" << endl; }

    A(const A& a): p(new int(*a.p)) { cout << "copy A()" << endl; }

    A(A&& a) {
        cout << "move A()" << endl;
        p = a.p;
        // asumming that move ctor steals A's resources leaving them in inconsistent state since:
        //
        // "While not mandated, move constructors usually leave the object that was moved out of in a valid state."
        // (c) https://stackoverflow.com/questions/27612289/move-constructor-not-calling-destructor/27612339
        // 
        // "not mandated" goes here
        a.p = (int*)INCONSISTENT; 
    }

    ~A() {
        cout << "~A()" << endl;
        if (p == (int*)INCONSISTENT) {
            cout << "oops, now will crash" << endl;
        }
        delete p;
     }
};

void sink1(unique_ptr<A> a)
{
    cout << "In sink1" << endl;
    unique_ptr<A> p(move(a));
}

void sink2(unique_ptr<A>&& a)
{
    cout << "In sink2" << endl;
    // Uncommenting this leads to same behavior as sink3 (i.e. no crashing)
    unique_ptr<A> p(move(a));
}

void sink3(A&& a)
{
    cout << "In sink3" << endl;
}

int main()
{
    unique_ptr<A> a1(new A());
    unique_ptr<A> a2(new A());
    A a3;

    cout << "--------------------" << endl;
    cout << "--------------------" << endl;
    cout << "--------------------" << endl;

    cout << "Before sink 1" << endl;
    sink1(move(a1));
    cout << "After sink 1" << endl;

    cout << "--------------------" << endl;
    cout << "--------------------" << endl;
    cout << "--------------------" << endl;

    cout << "Before sink 2" << endl;
    sink2(move(a2));
    cout << "After sink 2" << endl;    

    cout << "--------------------" << endl;
    cout << "--------------------" << endl;
    cout << "--------------------" << endl;

    cout << "Before sink 3" << endl;
    sink3(move(a3));
    cout << "After sink 3" << endl; 
}
  • В sink1 все правильно и хорошо, она «съедает» a1 и при этом даже сама его убивает, в случае чего
  • В sink2 оказывается, что само && ничего некуда не перемещает и если убрать вторую строчку, то деструктор a2 вызовется в конце scope main
  • Прочитав на stackoverflow, что move constructor не обязан оставлять объект, который ему передан в валидном состоянии, я попытался создать crash в sink3, но напоролся но простую штуку, которую не понял - то, что move ничего не перемещает, я понимаю, но оказывается что rvalue parameter тоже сам по себе не вызывает вовсе move конструктора!

Т.е. правильно ли я понимаю, что видя в коде:

void sink(A&& a);

я могу предположить, что автор f решает забрать ownership над a, но компилятор его вовсе не обязывает это делать? А где же тогда компилятор помогает здесь словить баг, например у меня есть указатель, я создал его с помощью new, я затем сделал move(*ptr), где ptr типа A и передал это нечто в функцию принимающую A&&, вроде sink(A&& a). Ну т.е.:

void sink(A&& a);
...
A* ptr = new A();
sink(move(*a));

По названию и сигнатуре я предпологаю, что таким образом, автор хочет забрать у меня A и не вызываю деструктора ptr образуя memory leak, ели внутри sink нету чего-то вроде:

A moved(move(a);

А я не знаю, есть оно там или нет (кода, например, у меня нет). Если бы было гарантировано, что деструкторы после конструктора move работают всегда, то я бы его вызвал, а так я боюсь, что sink() поломает мое содержимое ptr и не вызываю консруктора, а должен. Как же так?

Даже более того, вот еще более конкретный пример:

#include <iostream>
#include <memory>
#include <utility>
using namespace std;

const int INCONSISTENT = 666;

struct A {
    int *p;

    A(): p(new int(42)) { cout << "A()" << endl; }

    A(const A& a): p(new int(*a.p)) { cout << "copy A()" << endl; }

    A(A&& a) {
        cout << "move A()" << endl;
        p = a.p;
        // asumming that move ctor steals A's resources leaving them in inconsistent state since:
        //
        // "While not mandated, move constructors usually leave the object that was moved out of in a valid state."
        // (c) https://stackoverflow.com/questions/27612289/move-constructor-not-calling-destructor/27612339
        // 
        // while nullptr
        a.p = (int*)INCONSISTENT; 
    }

    ~A() {
        cout << "~A()" << endl;
        if (p == (int*)INCONSISTENT) {
            cout << "oops, now will crash" << endl;
        }
        delete p;
     }
};

void sink_fuckup(A&& a)
{
    cout << "In sink3" << endl;
    A my_a(move(a));
}

int main()
{
    A a3;

    cout << "Before sink_fuckup" << endl;
    sink_fuckup(move(a3));
    cout << "After sink_fuckup" << endl; 
}

Как же так? Автор же явно сказал мне, что он sink. А я не могу даже предотвратить вызов деструктора (из-за того, что a3 на stack). Более того, несмотря на то, что автор сказал мне, что он sink, компилятор этого не обеспечит (сама декларация sink_fuckup как принимающей A&& этого вовсе не гарантирует). Т.е. даже если бы a3 был pointer, проблема все равно осталась, так как передавая его в sink_fuckup, я не знал бы, вызывать delete a3 или нет. Кроме того, я иногда видел функции принимающие initializer_list<A>&& и затем делающие например move его членов в vector. Это ведь легальная оптимизация? Но при этом сам initializer_list останется «уничтожаем» и все хорошо. Но как я понял, гарантии, что он «уничтожаем» если он принят как rvalue reference нет никакой (ведь такая функция вполне может «забрать» его вызвав move конструктор, а ее деструктор оставит объект невалидным - ну может оставить). Получается, что не зная что делает объявленная где-то в хидере библиотеки функция sink(A&&), я, не имея комментария, о том, забирает ли она ownership не могу этого знать. Выходит это возвращение к sink(A*) // REMEMBER: sink takes ownership of a.

Или я туплю, как обычно?

★★

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

Ответ на: удаленный комментарий

Да ещё нужно помнить что семантика этой херни может изменится за пару лет. Хотя это наверно не про ОП, извините, просто нагорело.

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

семантика этой херни может изменится за пару лет

нет

annulen ★★★★★
()

А где же тогда компилятор помогает здесь словить баг, например у меня есть указатель, я создал его с помощью new

Если ты хочешь, чтобы компилятор тебе помогал, то тут ошибка: указатель надо было создавать с помощью std::make_unique. А с голыми указателями владение нужно разруливать вручную и семантика перемещения в этом никак не поможет

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

Т.е. имея external_library и external_library.h, но не имея external_library.cpp (например оно в so):

external_library.h

struct A {
   // see above
};
void sink(A&&); // defined in external_library.cpp not available

Вот этот код UB:

#include <external_library.h>
...
A *a = new A();
sink(*a);
delete a; // UB: can crash if sink moves A to smth (takes ownership)

И вот этот код potential memory leak:

#include <external_library.h>
...
A *a = new A();
sink(*a);
// leaks if sink does not move A to smth (does not take ownership)

Правильно я понял?

Или таки существует где-то гарантия, что move constructor должен оставить передаваемый объект в destructible state?

Меня последний вопрос интересует.

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

Еще хуже пример (external_library та же):

{
    A a;
    sink(move(a)); // UB? or not UB?
}

Хотя я вроде сам себе ответил: https://stackoverflow.com/questions/26579132/what-is-the-post-condition-of-a-.... Вроде valid state будет. Т.е. не UB.

Т.е. мое A нарушает женевскую (или еще какую-нибудь) конвенцию.

dissident ★★
() автор топика
Последнее исправление: dissident (всего исправлений: 3)
Ответ на: комментарий от anonymous

ОП? ООП? Причем здесь ООП. ООП is bad, это давно известно:

Правда в примере c GodObject и иерархией он забывает паттерны вроде Event Queue или Mediator, которые тоже это решают.

Меня не это интересует. Я просто был удивлен, что сам факт что sink выглядит так:

void sink(A&& a);

Вовсе не значит, что move constructor будет вызван, если я передаю туда «move'нутое» lvalue. Да и вообще даже rvalue, что не будет вызван был удивлен. Хотя прочел Modern Effective C++ Meyers'а.

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

Или таки существует где-то гарантия, что move constructor должен оставить передаваемый объект в destructible state?

Я не очень понимаю, о чем идет речь: в sink передается экземпляр класса А, возвращаемый выражением *a, а вовсе не переменная a, к которой ты пытаешься потом применить delete. Эти вещи вообще никак не связаны

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

И таки да, move constructor не имеет никаких обязательств по отношению к переданному в него объекту - может оставить как есть, может перевести его в неюзабельное состояние, может сделать что-то среднее. В общем, может сделать все, что может обычная функция сделать с переданным в нее rvalue. (Но в твоем случае это все относится к *a, а не а)

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

Ну был же выше другой пример где A a; sink(move(a)); // UB? Т.е. если я не гарантирую, что move constructor A оставляет переданное ему в destructible state то мне надо делать так: https://images-na.ssl-images-amazon.com/images/I/619nty5kQjL._SY606_.jpg

Тут передается переменная типа A, превращенная в rvalue при помощи move() и не зная, что вытворяет move ctor A нельзя сказать будет ли crash при выходе из scope или нет.

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

Ну блин ну еще раз пример же:

{
    A a;
    sink(move(a))
} // UB?

PS вроде бы move constructor A может делать все, что угодно, но должен оставить A destructible. Беру из моего google butt («The least guarantee provides the no move safety. The moved from object isn’t valid anymore. You can only call its destructor or assign it a new value.»). Я прав, что мое A очень плохое? И компилятор мне ничего не сказал.

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

Обидно, что компилятор, как обычно, не отрывает руки писателю A

Если не использовать голые указатели, вероятность факапа при написании конструкторов копирования и перемещения минимальна

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

В моем последнем примере вообще указателей нету. Есть только функция принимающая A&& и кривой A. В любом случае, ты ответил на мой вопрос про типичный пример. Если он должен работать, то A не должно в move contsructor делать

a.p = (int*)INCONSISTENT; 

Implicitly или explicitly.

PS Хотя нет, есть указатель, в A. С другой стороны, для pimpl использование unique_ptr ведет к какой-то трагедии с конструкторами если верить Scott Meyers (сейчас попробую найти).

Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.

Найти можно на libgen.io: http://libgen.io/search.php?req=meyers effective modern c++&lg_topic=libg... Но это так, если кому-то вдруг интересно, насколько введение unique_ptr для pimpl предотвращает копирование и сколько нужно нагородить огорода вокруг с конструкторами, чтобы это заработало.

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

Да, с pimpl все непросто по части копирования и перемещения, поэтому лучше не использовать pimpl без острой необходимости

annulen ★★★★★
()

все очень просто
вам нельзя писать на С++
попробуйте себя в чем то другом

anonymous
()
Ответ на: комментарий от dissident

А поделись пж, что за destructible state? Это твой собственный термин? По мне, так само это понятие бессмысленно, т.е. non-destructible state это просто бага, такого не должно быть. Move-конструктор всегда должен оставлять объект в destructible (хотя, возможно, и indeterminate) стейте.

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

Я собственно, это и справшивал и на это и получил ответ.

The least guarantee provides the no move safety. The moved from object isn’t valid anymore. You can only call its destructor or assign it a new value.

(c) https://foonathan.net/blog/2016/07/23/move-safety.html

Меня заинтересовало:

  • то, что передача параметра через T&& вовсе не вызывает move конструктора как я думал раньше
  • язык не гарантирует ничего из декларации T&&, поэтому для sink лучше ее не использовать (а использовать unique_ptr<T> а не unique_ptr<T>&& как предлагает Meyers), под «не гарантирует ничего» я имею ввиду, что конструкция fun(T&&) и даже fun(unique_ptr<T>&&) сама по себе ownership не забирает
  • Нигде никто никому не обещает, что move contrustor (из-за бага в себе например), не приведет объект к виду не «destructible» - вызов деструктора может привести к crash, resource leak, UB, если move constructor не написан правильно (то, что я сделал сознательно приписывая указателю мусор, но это же может произойти по ошибке
  • Слова «И таки да, move constructor не имеет никаких обязательств по отношению к переданному в него объекту - может оставить как есть, может перевести его в неюзабельное состояние, может сделать что-то среднее. В общем, может сделать все, что может обычная функция сделать с переданным в нее rvalue.» означают, что язык не представляет никакого механизма, а то, что move constructor оставит объект в состоянии уничтожабельном - просто здравый смысл

Собственно все. Я узнал что-то новое. Или Вы тот же анонимус, который троллил про UB при арифметике указателей над непроинизиализированной выровненной памятью, получаемой при помощи operator new()? Если да, то виртуально съедаю свою кепку второй раз. За то, что провоцируюсь на бессмысленный разговор ни о чем.

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

нене, я просто хотел узнать про destructible state. Ок.

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