Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing location of header file causes missing vtable error when compiling with CMake

Tags:

c++

cmake

qt

I need to transition from qmake to CMake for a large C++ project, but while working through a toy example I encountered some behavior that I don't understand. The example code features a single header file, and when that header file is moved into a subdirectory, I get a missing vtable error for the MainWindow class.

CMakeLists.txt

cmake_minimum_required(VERSION 2.6)
project(HelloCMake)

set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt5Widgets CONFIG REQUIRED)

include_directories("include")
set(INCLUDES include/mainwindow.h)
set(SOURCES
    main.cpp
    mainwindow.cpp
    mainwindow.ui
)

add_executable(hello-cmake ${SOURCES})   # error
# add_executable(hello-cmake ${SOURCES} ${INCLUDES})   # no error
target_link_libraries(hello-cmake Qt5::Widgets)

include/mainwindow.h (boilerplate)

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

mainwindow.cpp (boilerplate)

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow() {
    delete ui;
}

main.cpp (boilerplate)

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

Here's what I see when I run make (after first running cmake .):

[ 20%] Automatic MOC and UIC for target hello-cmake
[ 20%] Built target hello-cmake_autogen
[ 40%] Linking CXX executable hello-cmake
Undefined symbols for architecture x86_64:
  "vtable for MainWindow", referenced from:
      MainWindow::MainWindow(QWidget*) in mainwindow.cpp.o
      MainWindow::~MainWindow() in mainwindow.cpp.o
  NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]: *** [hello-cmake] Error 1
make[1]: *** [CMakeFiles/hello-cmake.dir/all] Error 2
make: *** [all] Error 2

If I add the header to the target by swapping the second add_executable command for the first one in CMakeLists.txt, the error goes away. I can also make the error go away by moving the header into the base directory with the .cpp files. However, I'd like to know what's actually going on here. I understand the general value of including the header files in the target, but why is a missing vtable error generated when I don't do this? Unless I grossly misunderstand, all the contents of mainwindow.h should be available while mainwindow.cpp is being compiled by virtue of the #include, whether or not the header is part of the add_executable statement.

-- EDIT

Here are the contents of ui_mainwindow.h, in case they're somehow relevant.

/********************************************************************************
** Form generated from reading UI file 'mainwindow.ui'
**
** Created by: Qt User Interface Compiler version 5.10.1
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/

#ifndef UI_MAINWINDOW_H
#define UI_MAINWINDOW_H

#include <QtCore/QVariant>
#include <QtWidgets/QAction>
#include <QtWidgets/QApplication>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStatusBar>
#include <QtWidgets/QToolBar>
#include <QtWidgets/QWidget>

QT_BEGIN_NAMESPACE

class Ui_MainWindow
{
public:
    QWidget *centralWidget;
    QPushButton *pushButton;
    QMenuBar *menuBar;
    QToolBar *mainToolBar;
    QStatusBar *statusBar;

    void setupUi(QMainWindow *MainWindow)
    {
        if (MainWindow->objectName().isEmpty())
            MainWindow->setObjectName(QStringLiteral("MainWindow"));
        MainWindow->resize(393, 307);
        centralWidget = new QWidget(MainWindow);
        centralWidget->setObjectName(QStringLiteral("centralWidget"));
        pushButton = new QPushButton(centralWidget);
        pushButton->setObjectName(QStringLiteral("pushButton"));
        pushButton->setGeometry(QRect(10, 10, 113, 32));
        MainWindow->setCentralWidget(centralWidget);
        menuBar = new QMenuBar(MainWindow);
        menuBar->setObjectName(QStringLiteral("menuBar"));
        menuBar->setGeometry(QRect(0, 0, 393, 22));
        MainWindow->setMenuBar(menuBar);
        mainToolBar = new QToolBar(MainWindow);
        mainToolBar->setObjectName(QStringLiteral("mainToolBar"));
        MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar);
        statusBar = new QStatusBar(MainWindow);
        statusBar->setObjectName(QStringLiteral("statusBar"));
        MainWindow->setStatusBar(statusBar);

        retranslateUi(MainWindow);
        QObject::connect(pushButton, SIGNAL(released()), pushButton, SLOT(hide()));

        QMetaObject::connectSlotsByName(MainWindow);
    } // setupUi

    void retranslateUi(QMainWindow *MainWindow)
    {
        MainWindow->setWindowTitle(QApplication::translate("MainWindow", "MainWindow", nullptr));
        pushButton->setText(QApplication::translate("MainWindow", "Do not press", nullptr));
    } // retranslateUi

};

namespace Ui {
    class MainWindow: public Ui_MainWindow {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_MAINWINDOW_H
like image 558
Biennial Avatar asked Oct 25 '25 15:10

Biennial


2 Answers

I was able to migrate my real project to CMake with exactly one hiccup: the linker wasn't happy with any of the signal functions in my QObject-derived classes. (For those not familiar with Qt, a meta-object compiler, moc, magically converts certain empty functions marked as signals in a header file into real functions that the C++ compiler can use.) My conclusion is that both problems resulted from CMake not seeing the header files for a QObject-derived class, and so they were not sent to the moc in spite of the CMAKE_AUTOMOC setting.

The upshot is, if moc (or uic or rcc) needs to compile a file, then CMake has to know it exists before building any dependent targets. The crude solution I used for my project was to grep Q_OBJECT in the directory with all my header files, and copy/paste this list into a long set command in CMakeLists.txt

set(MOC_SOURCES
    include/applewidget.h
    include/borangewidget.h
    ...
)

and then add ${MOC_SOURCES} to my add_executable line. There may be a more sophisticated solution that involves building these objects separately, but my use of CMake has not yet reached that level of sophistication.

like image 78
Biennial Avatar answered Oct 28 '25 03:10

Biennial


Meta object compiler (moc) generates moc_*.cpp files from headers. So the build system needs to know which headers to feed it. Similar to C++ compiler that needs to know which files to process. So when working with CMake and Qt you need to consider the header files as source files. CMake is smart enough to not ask C++ compiler to compile them so there's no harm in adding them for other libraries too.

Unfortunately neither Qt-CMake nor CMake-Qt documentation state this explicitly but you should always add headers to the list of sources in add_executable/add_library for CMAKE_AUTOMOC to work.


Another important thing is the minimal required version of CMake. CMake 2.6 does not support Qt5. Neither does CMake 2.8. The official Qt documentation claims the minimal CMake version to be 3.1.0. However CMake 3.0 already has Qt5 support. Still those version are way too old, slow, and buggy so consider updating the requirement. I'd recommend 3.11 or latest stable.

like image 33
Teivaz Avatar answered Oct 28 '25 03:10

Teivaz