Перемещение локальной переменной типа абстрактного класса из виртуального метода
Требуется реализовать более одного класса с перегрузками арифметических операторов, чтобы с ними можно было работать через указатель/ссылку на абстрактный класс от которого они наследованы. Обычно бинарные арифметические операторы такие как +, -, *, /, % возвращают копию объекта, сконструированную и изменённую внутри перегрузки оператора, таким образом в простейшем случае сигнатура перегрузки любого такого оператора для класса A выглядит следующим образом: A operator+(int v) { A tmp(*this); tmp.v += v; return tmp; }.
В качеств примера есть следующий код:
#include <iostream>
// Abstract class
struct Base {
virtual ~Base() {};
virtual Base&& operator+(int) = 0;
virtual Base& operator+=(int) = 0;
};
// Implementation
struct Derived : public Base {
int a = 0;
Derived() { std::cout << "<default ctor>\n"; }
Derived(Derived&& other) {std::cout << "<move d ctor " << other.a << " to " << a << ">\n"; a = other.a; }
Derived(Derived& other) {std::cout << "<copy d ctor " << other.a << " to " << a << ">\n"; a = other.a; }
Derived(Base&& other) {std::cout << "<move b ctor " << dynamic_cast<Derived&&>(other).a << " to " << a << ">\n"; a = dynamic_cast<Derived&&>(other).a; }
Derived(Base& other) {std::cout << "<copy b ctor " << dynamic_cast<Derived&>(other).a << " to " << a << ">\n"; a = dynamic_cast<Derived&>(other).a; }
virtual ~Derived() {std::cout << "<dctor of " << a << ">\n";}
virtual Derived&& operator+(int v) override {
std::cout << "<+ from " << a << ' ';
Derived tmp(*this);
tmp.a += v;
std::cout << " to " << tmp.a << ">\n";
return std::move(tmp);
}
virtual Derived& operator+=(int v) override {
std::cout << "<+= from " << a << ' ';
a += v;
std::cout << " to " << a << ">\n";
return *this;
}
};
int main() {
Base* a = new Derived(); // <default ctor>
*a += 5; // <+= from 0 to 5>
Base* b = new Derived(*a + 10); // <+ from 5 <copy d ctor 5 to 0> to 15> <dctor of 15> <move b ctor 15 to 0>
// (ERROR: dctor of tmp before move ctor to outer var!!!)
std::cout
<< "a = " << dynamic_cast<Derived*>(a)->a
<< "; b = " << dynamic_cast<Derived*>(b)->a
<< '\n'; // a = 5; b = 15
delete a; // <dctor of 5>
delete b; // <dctor of 15>
return 0;
}
Проблема 1: если мы напишем virual Base operator+(int) = 0; компилятор/статический анализатор закономерно ругаются, мол нельзя возвращать абстрактный класс по значению, что в принципе логично, как следствие меняем возврат по значение Base на возврат по rvalue-ссылке Base&&, что казалось бы должно помочь, но...
Проблема 2: компиляторы очень интересным образом обрабатывают явно указанный возврат по rvalue-ссылке из методов. Обращаю внимание на 3-ю строку функции main. Ситуация следующая:
- Запускается метод
Base&& Base::operator+(int)->Derived&& Derived::operator+(int)- Конструируется объект класса
Derivedс именемtmpчерез конструктор копирования от*thisна стэке tmpизменяется (tmp.a += v)tmpдеконструируется
- Конструируется объект класса
- Внешний объект конструируется через конструктор перемещения от
tmpкоторый уже уничтожен
Таким образом мы получаем UB, когда объект конструируется от куска неинициализированной памяти, да ещё и с освобождённой части стэка.
Если кто знает - возможно ли это как-либо сделать без UB?
Ответы (2 шт):
Виртуальный объект по значению возвращать не получится. Можно только ссылкой на выделенную память. Сделал класс BaseTemp указателей на временный объект, который автоматически удаляется.
Как аналог std::unique_ptr<Base>
#include <iostream>
struct Base ;
struct BaseTemp {
Base * p ;
BaseTemp(Base*x):p{x}{}
~BaseTemp();
} ;
// Abstract class
struct Base {
virtual ~Base() {}
virtual BaseTemp operator+(int)=0;
virtual Base& operator+=(int) = 0;
};
BaseTemp::~BaseTemp(){
std::cout << "<BaseTemp dctor " << p << ">\n";
delete p;
}
// Implementation
struct Derived : public Base {
int a = 0;
Derived() { std::cout << "<default ctor>\n"; }
Derived(BaseTemp bt):a{static_cast<Derived*>(bt.p)->a} { std::cout << "<BaseTemp ctor>\n"; }
Derived(Derived&& other) {std::cout << "<move d ctor " << other.a << " to " << a << ">\n"; a = other.a; }
Derived(Derived& other) {std::cout << "<copy d ctor " << other.a << " to " << a << ">\n"; a = other.a; }
Derived(Base&& other) {std::cout << "<move b ctor " << dynamic_cast<Derived&&>(other).a << " to " << a << ">\n"; a = dynamic_cast<Derived&&>(other).a; }
Derived(Base& other) {std::cout << "<copy b ctor " << dynamic_cast<Derived&>(other).a << " to " << a << ">\n"; a = dynamic_cast<Derived&>(other).a; }
virtual ~Derived() {std::cout << "<dctor of " << a << ">\n";}
virtual BaseTemp operator+(int v) {
std::cout << "<+ from " << a << ' ';
Derived * tmpp = new Derived(*this) ;
tmpp->a += v;
std::cout << " to " << tmpp->a << ">\n";
return tmpp;
}
virtual Derived& operator+=(int v) override {
std::cout << "<+= from " << a << ' ';
a += v;
std::cout << " to " << a << ">\n";
return *this;
}
};
int main() {
Base* a = new Derived(); // <default ctor>
(*a) += 5; // <+= from 0 to 5>
Base* b = new Derived((*a) + 10); // <+ from 5 <copy d ctor 5 to 0> to 15> <dctor of 15> <move b ctor 15 to 0>
// (ERROR: dctor of tmp before move ctor to outer var!!!)
std::cout
<< "a = " << dynamic_cast<Derived*>(a)->a
<< "; b = " << dynamic_cast<Derived*>(b)->a
<< '\n'; // a = 5; b = 15
delete a; // <dctor of 5>
delete b; // <dctor of 15>
return 0;
}
Пока вроде пашет. Проверьте сами..
Проблема в том, что вы забыли про управление памятью.
Рассмотрим ваш оператор virtual Base?? operator+(int) = 0; подробнее, с позиции "что он вообще должен вернуть". А вернуть-то он обязан новый объект! Но если объект новый, и к тому же неизвестного размера (virtual же!) - кто-то должен выделить для него память, а потом освободить её.
Для управления памятью в С++ принята идиома RAII в общем и умные указатели в частности. К примеру, тут подойдёт std::unique_ptr:
struct Base {
virtual ~Base() {}
virtual std::unique_ptr<Base> operator+(int) =0;
virtual Base& operator+=(int) =0;
};
Однако, хоть такой вариант и будет работать - он странный. В нём результат операции сложения невозможно снова сложить, вместо условного x + 5 + 6 придётся писать *(x+5) + 6.
Это связано с более фундаментальной ошибкой - смешиванием механизмов, предназначенных для работы с объектами как значениями (операторы) и для работы с объектами как ссылками (полиморфизм).
Решение? Нужно разделить эти объекты! Пусть один класс отвечает за полиморфизм, а второй - за перегрузку операторов.
Например, вот так:
struct Base {
virtual ~Base() {}
virtual std::unique_ptr<Base> Clone() =0;
virtual void Add(int) =0;
}
class base_ptr {
std::unique_ptr<Base> ptr; // или shared_ptr
public:
explicit base_ptr(std::unique_ptr<Base> ptr) : ptr(ptr) {}
// …
base_ptr& operator += (int arg) {
ptr->Add(arg);
return *this;
}
base_ptr operator + (int arg) {
auto clone = ptr->Clone();
clone->Add(arg);
return base_ptr(std::move(clone));
}
base_ptr operator + (int arg) && {
ptr->Add(arg);
return base_ptr(std::move(ptr));
}
}