Начитавшись:
- https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/
- http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed...
Я собственно попробовал и так и так:
#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.
Или я туплю, как обычно?