-2

Take this example code:

#include <QWidget>
#include <QMainWindow>
#include <QList>
#include <QGridLayout>
#include <QLabel>
#include <QDialogButtonBox>
#include <QApplication>

class MainWindow;

class BaseWidget : public QWidget
{
public:
    BaseWidget() = default;
    virtual ~BaseWidget() {};

    virtual void display(MainWindow *window) {}
};

class MainWindow : public QMainWindow
{
private:
    QList<BaseWidget *> queue;
public:
    void setWidget(BaseWidget *widget) {
        if (queue.isEmpty() || queue.last() != widget) {
            queue += widget;
        }
        this->setCentralWidget(widget);
    }
public slots:
    void back() {
        queue.removeLast();
        this->setWidget(queue.last());
    }
};

class DerivedWidget2 : public BaseWidget
{
public:
    void display(MainWindow *window) {
        QGridLayout *layout = new QGridLayout(this);
        QLabel *label = new QLabel(tr("hit \"Ok\" to segfault"));
        layout->addWidget(label);

        QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok);
        connect(box, &QDialogButtonBox::accepted, window, &MainWindow::back);
        layout->addWidget(box);

        window->setWidget(this);
    }
};

class DerivedWidget : public BaseWidget
{
public:
    void display(MainWindow *window) {
        QGridLayout *layout = new QGridLayout(this);
        QLabel *label = new QLabel(tr("hit \"Ok\" to move to the next"));
        layout->addWidget(label);

        QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok);
        connect(box, &QDialogButtonBox::accepted, window, [window] {
            DerivedWidget2 *widget2 = new DerivedWidget2;
            widget2->display(window);
        });
        layout->addWidget(box);

        window->setWidget(this);
        window->show();
    }
};

int main(int argc, char **argv) {
    QApplication app(argc, argv);

    MainWindow *window = new MainWindow;
    DerivedWidget *widget = new DerivedWidget;

    widget->display(window);
    return app.exec();
}

(Note that I've got a lot more derived widgets, so no I'm not gonna put a bunch of ifs and dynamic_casts. Also, all of this stuff would be in completely separate scopes/files.)

What I'm doing is storing these derived widgets in a "queue", where the last one is the currently displayed one, and you can go "back" in the queue, which will set the MainWindow's central widget (i.e., which widget it displays to the user) to the previous widget in the queue.

So, everything seems to work fine, except one thing. When the widget is being displayed, in the queue list it stays as the derived class (according to my debugger). However, when I change the displayed widget, any widgets in the queue not currently being displayed seem to "degrade" into the base class (or at least that's what my debugger says), and it for some reason loses every single one of its properties. So, when I try to do something with that widget (like going back in the queue, back to that widget, and setting the main window to display it), it just segfaults.

Oddly enough, however, when I look in my debugger, there are other instances of that exact same widget pointer, pointing to the exact same memory address - they're just of the subclass type, while in the list it's "degraded" to the superclass.

Why is this happening? Anything I can do to fix it?

EDIT: Here's a video showing this with the debugger, should hopefully make it more clear.

EDIT 2: When the BaseWidget class has actual members, when this segfault happens those members seem to all completely deallocate and "die", or turn into complete garbage in certain cases.

roach
  • 84
  • 1
  • 8
  • 3
    Please provide a [mcve] showing how the queue is filled and how the widgets are being displayed. Sounds like you are possibly [slicing](https://stackoverflow.com/questions/274626/) the objects. – Remy Lebeau Mar 03 '21 at 21:43
  • @RemyLebeau done, and I'm pretty sure it's not slicing, because I'm storing pointers to the widgets. – roach Mar 03 '21 at 22:20
  • 1
    From doc: QMainWindow::setCentralWidget(QWidget *widget) Note: QMainWindow takes ownership of the widget pointer and deletes it at the appropriate time. – Shtol Krakov Mar 19 '21 at 06:08

1 Answers1

1

As someoneinthebox commented, Qt doc about QMainWindow::setCentralWidget says:

Note: QMainWindow takes ownership of the widget pointer and deletes it at the appropriate time.

So, second time you do this->setCentralWidget(widget);, the widget that was previously used as central widget gets automatically deleted by Qt (See Memory management in Qt?). So when you try to go back, you set as central widget a deleted widget, so you obviously get a segmentation fault. You can observe that by setting a breakpoint in BaseWidget destructor, you'll hit the breakpoint on second call to this->setCentralWidget(widget);.

So you need to have permanent widget as central widget and attach the BaseWidget derived class items as child of it (and prevent them from being automatically deleted by Qt). I propose to use QStackedLayout which allows to store many widgets in a layout and have only one be visible (but there could be other working strategies):

Your MainWidget class would then be:

class MainWindow : public QMainWindow
{
private:
    QList<BaseWidget *> queue;
    QWidget* centralWidget;
    QStackedLayout* stackedLayout;
    
public:
    MainWindow()
    {
        this->setCentralWidget(centralWidget = new QWidget());
        centralWidget->setLayout( stackedLayout = new QStackedLayout() );
        stackedLayout->setContentsMargins(0,0,0,0);
    }

    void setWidget(BaseWidget *widget) {
        if (queue.isEmpty() || queue.last() != widget) {
            queue += widget;
        }

        // old code (segfault:
        // this->setCentralWidget(widget);

        // new code:
        if ( stackedLayout->indexOf( widget ) == -1 )
            stackedLayout->addWidget( widget );
        // widget already in stackedLayout (not the first time it is being shown)

        stackedLayout->setCurrentWidget( widget );
    }
public slots:
    void back() {
        queue.removeLast();
        this->setWidget(queue.last());
    }
};

Then it does not crash anymore.

jpo38
  • 20,821
  • 10
  • 70
  • 151
  • All these years of staring at man pages, documentation for libraries, and other similar text and I still didn't notice the deletion part, lol. I tried using QStackedWidget before but it didn't seem to work, never actually tried QStackedLayout instead. This does seem to no longer crash, so thanks! I'll be testing this with the code I need to use it with to see if it works and if so I'll accept this answer. once again thanks! – roach Mar 20 '21 at 13:29