Перемещение локальной переменной типа абстрактного класса из виртуального метода

Требуется реализовать более одного класса с перегрузками арифметических операторов, чтобы с ними можно было работать через указатель/ссылку на абстрактный класс от которого они наследованы. Обычно бинарные арифметические операторы такие как +, -, *, /, % возвращают копию объекта, сконструированную и изменённую внутри перегрузки оператора, таким образом в простейшем случае сигнатура перегрузки любого такого оператора для класса 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 шт):

Автор решения: AlexGlebe

Виртуальный объект по значению возвращать не получится. Можно только ссылкой на выделенную память. Сделал класс 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;
}

Пока вроде пашет. Проверьте сами..

→ Ссылка
Автор решения: Pavel Mayorov

Проблема в том, что вы забыли про управление памятью.

Рассмотрим ваш оператор 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));
    }
}
→ Ссылка