1
\$\begingroup\$

Searching for a solution to my problem I've read many posts regarding templates in Qt (including those on SE) and didn't find a complete solution (or I should say a complete example of a solution) so this post. It is not only to review the code but I think it could be useful for others with similar needs.

Motivation

I have a library that provides config structs for different modules like this :

struct UConfig<V2718> // V2718 is a class representing the V895 module
{
    // V2718 specific parameters
};

In the library I also have two functions:

template <typename M_TYPE>
WriteConfigToFile( const UConfig<M_TYPE>& cfg, /*...*/ )

template <typename M_TYPE>
ReadConfigFromFile( UConfig<M_TYPE>& cfg, /*...*/ )

which serialize/deserialize config-struct and write/read it to/from a file.

And I am developing a GUI for these modules with Qt. For every such module there is a window widget that provides interface to the module. The thing is those windows have something in common : for example, every such window have a menu that allows a user to save/load configuration to/from a file. And from the above we conclude that the procedure should be the same regardless the module (window). The scheme is the following (writing):

enter image description here

Of course, the CreateConfig() function is module-specific, but the scheme is kind of polymorphic. And this is true for everything that involves UConfig<M_TYPE>. So the obvious solution would be for every concrete window to inherit from a class template:

template <typename M_TYPE>
class ModuleWindow : public QWidget
{
    Q_OBJECT

    virtual UConfig<M_TYPE> CreateConfig() = 0;
};

class V2718Window : public ModuleWindow<V2718>
{
    Q_OBJECT

    UConfig<V2718> CreateConfig() override;
};

Oops! Qt's Meta Object Compiler doesn't support class templates.

Solution

The solution I'm suggesting is to mix the QVariant and template member functions, and the fact that although you cannot have template slots you can connect the template instance to a signal.

Code

The presented code is a Minimal Reproducible Example.

config.h

This file simulates a library : something that you cannot (or don't want to) change and what you want to work with.

#include <iostream>
#include <typeinfo>

template <typename M_TYPE>
struct Config;

template<>
struct Config<int> { int a; }; // <--- config for the Int window

template<>
struct Config<double> { double a; }; // <--- config for the Double window

template <typename M_TYPE>
void WriteConfigToFile( const Config<M_TYPE>& cfg ) // Doesn't actually write anything to a file
{
    std::cout << "The a field of " << typeid( cfg ).name() << "is " << cfg.a << "\n";
}

test.h

#include <QWidget>
#include <QVariant>

#include "config.h"

/* This is necessary to represent our configs as QVariant */
Q_DECLARE_METATYPE(Config<int>)
Q_DECLARE_METATYPE(Config<double>)


class QPushButton;

class Base : public QWidget
{
    Q_OBJECT

    protected :
        QPushButton* fButton;

        virtual QVariant CreateConfig() = 0; // <--- builds the config from widgets' data and puts it in QVariant

        template <typename M_TYPE>
        void SaveConfig() // <--- The procedure to save config : implemented once and for all
        {
            Config<M_TYPE> cfg = qvariant_cast<Config<M_TYPE>>( this->CreateConfig() );
            WriteConfigToFile( cfg );
        }

    public :
        Base( QWidget* parent = nullptr );
        virtual ~Base();
};


class Int : public Base
{
    protected :
        QVariant CreateConfig() override;

    public :
        Int( QWidget* parent = nullptr );
        virtual ~Int();
};

class Double : public Base
{
    protected :
        QVariant CreateConfig() override;

    public :
        Double( QWidget* parent = nullptr );
        virtual ~Double();
};

test.cpp

#include <QPushButton>
#include <QHBoxLayout>

#include "test.h"

Base::Base( QWidget* parent ) :
    QWidget( parent )
{
    /* Create the window skeleton : the button to save config
     * BUT not connect its click to anything - it will be done
     * in the concrete module class */
    QHBoxLayout* layout = new QHBoxLayout();
    fButton = new QPushButton( "Write config to file" );
    layout->addWidget( fButton );
    setLayout( layout );
}

Base::~Base() { };


Int::Int( QWidget* parent ) :
    Base( parent )
{
    connect( fButton, &QPushButton::clicked, this, &Base::SaveConfig<int> ); // <--- this window knows the M_TYPE so connect
}

Int::~Int() { }

QVariant Int::CreateConfig()
{
    Config<int> cfg;
    cfg.a = 6;
    QVariant qv;
    qv.setValue( cfg );
    return qv;
}

Double::Double( QWidget* parent ) :
    Base( parent )
{
    connect( fButton, &QPushButton::clicked, this, &Base::SaveConfig<double> );
}

Double::~Double() { }

QVariant Double::CreateConfig()
{
    Config<double> cfg;
    cfg.a = 28.0;
    QVariant qv;
    qv.setValue( cfg );
    return qv;
}

The main function is trivial. To run :

qmake -project "QT += widgets"
qmake
make
\$\endgroup\$
7
  • \$\begingroup\$ "The presented code is a MRE." The only meaning of MRE that I know is en.wikipedia.org/wiki/Meal,_Ready-to-Eat . I don't find any code-related meanings for such an acronym. Is it a Qt thing? \$\endgroup\$
    – JDługosz
    Sep 21 at 15:13
  • \$\begingroup\$ Just a thought: Does Copperspice work any better? "All functionality originally provided by moc was replaced with compile time templates" ... "A template class can now inherit from QObject with no restrictions on data types" \$\endgroup\$
    – JDługosz
    Sep 21 at 15:25
  • 1
    \$\begingroup\$ @JDługosz, haha :) Sorry I thought the acronym is very popular on SE. Edited. \$\endgroup\$
    – LRDPRDX
    Sep 21 at 16:59
  • \$\begingroup\$ @JDługosz, (on Copperspice), thank you for a recommendation. I will consider this library in the future, but I see it requires the C++17 standard which is unavailable for now on our relatively old machines. \$\endgroup\$
    – LRDPRDX
    Sep 22 at 10:15
  • \$\begingroup\$ you know, a new compiler can be installed... it doesn't matter how old the machine is. \$\endgroup\$
    – JDługosz
    Sep 22 at 13:54
1
\$\begingroup\$

Not Qt related but...

See Core Guidelines C.128.

virtual ~Int();
should be:
~Int() override =default;

Your definitions of empty destructors (e.g.)

Double::~Double() { }

are de-optimizations in two ways. First, they are non-inline and not in the header, so (unless you have working whole-program link-time code generation) the pointless function call will be made every time. Second, the compiler "understands" the generated destructor better and and further optimize. So, use =default whenever you need to explicitly specify that, and don't write an empty destructor.

\$\endgroup\$
1
  • \$\begingroup\$ Cool! Thank you. Should've known this. \$\endgroup\$
    – LRDPRDX
    Sep 21 at 17:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.