Комментарии 19
Тогда пользователь объекта, который вдруг воспользуется функцией getLength, получит неверные сведения. Поэтому нельзя варварски обращаться с объектом, который уже не нужен.
Вообще говоря, можно. Даже стандартная библиотека, которая гарантирует "действительное, но не оговорённое" (valid but unspecified) состояние перемещённого объекта (раздел "Moved-from state of library types" [lib.types.movedfrom]), делает это исключительно по своей доброй воле.
Для обычного объекта вполне достаточно того, что перемещённый объект можно уничтожить (выполнить деструктор) или заново проинициализировать (выполнить любое присвоение).
Потому что в целом было бы странно продолжать нормальную работу с объектами, из которых вытащили все внутренности.
Что если я хочу написать так?
class T
{
public:
T();
T( const T& ) = delete;
};
T&& foo()
{
return T();
}
T&& t = foo();
Фактически я хочу легализовать поведение компилятора, когда место под объект выделяется в стеке вызывающей функции, а вызов конструктора происходит в вызываемой функции.
При этом я не хочу разрешать перемещать или копировать объект, или использовать placement new (не хочу руками вызывать деструктор).
На секунду появилась мысль написать так:
void bar(T& t)
{
t.T::T();
}
Но это не вариант, непонятно на что ссылаться. Мне неизвестно как создать объект с выделением памяти и вызовом деструктора, но без вызова конструктора.
Парадоксально, что судя по вашей статье именно это и происходит (выделение на стеке вызывающей функции, конструирование в вызываемой функции), но в 2017 году до сих пор нет варианта легализовать данное поведение.
Мы всё ещё делаем вид, что есть какое-то мнимое копирование, сдувая пыль со стандартов 20-летней давности.
Я даже не уверен, что если я напишу конструктор перемещения, то это не сделает ситуацию ещё хуже, вынудив компилятор отказаться от RVO. Это какой-то провал!
На всякий случай, чтобы не быть голословным. В main:
main:
lea rax, [rbp-17] //выделили место на стеке
mov rdi, rax //передаем адрес через rdi
call foo()
lea rax, [rbp-17]
mov rdi, rax
call T::~T() //вызов деструктора для того же адреса
теперь смотрим foo:
foo:
mov QWORD PTR [rbp-8], rdi //копирование туда-сюда
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call T::T() //вызов конструктора для адреса
//в rdi в стеке main
Иногда действительно хочется отложенную инициализацию.
Фактически я хочу легализовать поведение компилятора, когда место под объект выделяется в стеке вызывающей функции, а вызов конструктора происходит в вызываемой функции.
Это невозможно легализовать средствами языка (синтаксически), поскольку в стандарте C++ стек вообще не упоминается применительно к вызову функций, и компиляторы вольны реализовывать функции, как им угодно.
Однако, в C++17 все будет работать так, как вы хотите, и не иначе: возвращаемые значения будут сразу конструироваться на нужном месте (guaranteed copy elision).
Хороший комментарий. Вот, что интересно: если вы разбирались с этим — заданы ли четкие требования к работе copy elision? Например, в таком случае:
T foo()
{
T t;
if (rand() % 2)
{
T t;
return t;
}
else
{
return t;
}
}
Подробное введение в rvalue-ссылки для тех, кому не хватило краткого