有没有比metafactory更好的方法来解决CRTP中派生类的构造函数注入问题?

Rei*_*ica 11 c++ qt c++11

CRTP中,我想干净地将构造函数注入到派生类中 - 不使用宏而不将其写出来.这似乎是不可能的,所以我想出了一些解决方法.

首先,有一个底层事件类(QEvent),它应该为每个派生类都有一个唯一的整数类型标记(参见基本原理).你可以通过调用注册函数来获取它很容易创建一个CRTP包装器来隐藏它:

template <typename Derived> class EventWrapper : public QEvent {
public:
    EventWrapper() : QEvent(staticType()) {}
    static QEvent::Type staticType() {
        static QEvent::Type type = static_cast<QEvent::Type>(registerEventType());
        return type;
    }
};
class MyEvent1 : public EventWrapper<MyEvent1> {}; // easy-peasy
class MyEvent2 : public EventWrapper<MyEvent2> {};
Run Code Online (Sandbox Code Playgroud)

请注意MyEvent1::staticType() != MyEvent2::staticType():registerEventType()每次调用时都会返回唯一类型.

现在我希望事件类携带一些数据:

template <typename Derived> class StringEvent : public EventWrapper<D> {
    std::string m_str;
public:
    explicit StringEvent(const std::string & str) : m_str(str) {}
    std::string value() const { return m_str; }
};
Run Code Online (Sandbox Code Playgroud)

但是在这里我们遇到了一个问题:我们需要在每个派生类中手动定义构造函数.这里的重点是创建这样的类应该很容易,因为可能有许多不同的字符串携带事件类型.但这简直容易:

class MyEvent3 : public StringEvent<MyEvent3> {
    public: MyEvent3(std::string s) : StringEvent(s) {}
};
Run Code Online (Sandbox Code Playgroud)

即使使用C++ 11构造函数转发,这显然也很快变旧:

class MyEvent3 : public StringEvent<MyEvent3> { using StringEvent::StringEvent; };
Run Code Online (Sandbox Code Playgroud)

我们想要的是一种将这个构造函数注入到派生类中的方法,或者在提供易用性的同时避免这样做.当然你可以将它隐藏在预处理器宏中,但我讨厌那些宏,它们是一种维护痛苦,因为它们为非常简单的概念引入了新的名称.

我们当然可以使用虚拟类型.请注意,不需要虚拟类型的定义.它只是一个用作类型参数的名称.

// Pre-C++11
class DummyEvent3;
typedef StringEvent<DummyEvent3> MyEvent3;
// C++11
class DummyEvent3;
using MyEvent3 = StringEvent<DummyEvent3>;
Run Code Online (Sandbox Code Playgroud)

另一个解决方案是使用int模板参数并使用枚举值,但这会带回通过首先使用它解决的唯一性问题registerEventType().保证大型程序是正确的并不是一件好事.你仍然需要拼出枚举.

所以,我想出了一个metaprogram类,我将其称为metafactory,它可以StringEvent为我们生成即用型类,同时将它保留为一个类型定义:

// the metafactory for string events
template <typename Derived> class StringEventMF {
public:
    class Event : public EventWrapper<Derived> {
        std::string m_str;
    public:
        explicit Event(const std::string & val) : m_str(val) {}
        std::string value() const { return m_str; }
    };
};
Run Code Online (Sandbox Code Playgroud)

或者干脆

template <typename Derived> class StringEventMF {
public:
    typedef StringEvent<Derived> Event;
};
Run Code Online (Sandbox Code Playgroud)

使用方式如下:

class Update : public StringEventMF<Update> {};
class Clear : public StringEventMF<Clear> {};

void test() {
   Update::Event * ev = new Update::Event("foo");
   ...
}
Run Code Online (Sandbox Code Playgroud)

你使用的课程是Update::Event,Clear::Event.这是UpdateClearmetafactories:它们为我们生成了所需的事件类.metafactory的推导避开了具体类类型的推导.metafactory类型给出了创建唯一具体类类型所需的唯一类型鉴别器.

问题是:

  1. 这样做有"更清洁"或"更理想"的方式吗?理想情况下,以下非工作伪代码将是我理想的做法 - 零重复:

    class UpdateEvent : public StringEvent <magic>;
    
    Run Code Online (Sandbox Code Playgroud)

    派生类的名称StringEvent只出现一次,基本概念的名称也只出现一次.CRTP要求类名出现两次 - 到目前为止我认为它是可以接受的,但我的元编程功能已经破败了.同样,我想要一个无预处理器的解决方案,否则它将是一个明智的选择.

  2. 名称metafactory是我的原始发明(哈哈),还是仅仅是我缺乏的谷歌?这种元素模式似乎非常灵活.通过多次推导可以很容易地组合元形式.假设你想要Update::Event一个工厂生产,Update::Foo另一个工厂制造.

这个问题是由这个答案推动的.注意:在实际代码中我会使用QString,但我试图尽可能保持通用.

Rei*_*ica 3

Yochai Timmer 提出了一种解决该问题的替代方法。他不必从数据载体类转发构造函数,而是公开一个生成伪派生类的工厂方法。由于它会调用未定义的行为,因此我并不是特别热衷于它。

对原始元工厂概念进行一些扩展,可以创建通用元工厂,该通用元工厂可用于创建包装“任何”数据携带类的独特事件类型。

C++11 的方法使用构造函数转发,以便可以使用普通的非模板数据载体类。C++98 的方法需要模板化的数据载体类,并且在内部需要更多的体操,但它也有效。

事件类不能进一步从 派生。这是必要的,因为派生类都将共享 的值staticType,而这是不允许的,正如 DyP 在评论中适当指出的那样。

要测试代码,您需要事件包装器、为 C++ 变体选择的元工厂和数据载体以及测试/使用部分。

事件包装器(通用代码)

无论哪种情况,为事件生成唯一静态类型值的基本事件包装 CRTP 类是:

// A type-identifier-generating wrapper for events. It also works with RTTI disabled.
template <typename Derived> class EventWrapper : public QEvent {
public:
    EventWrapper() : QEvent(staticType()) {}
    static QEvent::Type staticType() {
        static QEvent::Type type = static_cast<QEvent::Type>(registerEventType());
        return type;
    }
    static bool is(const QEvent * ev) { return ev->type() == staticType(); }
    static Derived* cast(QEvent * ev) { return is(ev) ? static_cast<Derived*>(ev) : 0; }
};
Run Code Online (Sandbox Code Playgroud)

请注意,它还提供了强制转换为派生的方法。您可以在事件处理程序中使用它,并给出指向基本事件类的指针:

void MyClass::customEvent(QEvent* event) {
   if (MyEvent::is(event)) {
      auto myEvent = MyEvent::cast(event);
      // use myEvent to access data carrying members etc)
   }
}
Run Code Online (Sandbox Code Playgroud)

C++98 元工厂

Carrier是参数化的数据载体类,如下所示StringData

// The generic event metafactory
template <typename Derived, template <typename> class Carrier> class EventMF {
    class EventFwd;
    class Final;
    class FinalWrapper : public EventWrapper<EventFwd>, public virtual Final {};
public:
    // EventFwd is a class derived from Event. The EventWrapper's cast()
    // will cast to a covariant return type - the derived class. That's OK.
    typedef Carrier<FinalWrapper> Event;
private:
    class EventFwd : public Event {};
    class Final {
        friend class FinalWrapper;
        friend class Carrier<FinalWrapper>;
    private:
        Final() {}
        Final(const Final &) {}
    };
};
Run Code Online (Sandbox Code Playgroud)

需要该类EventFwd,以便我们可以将一些合理的内容EventWrapper作为派生类传递给模板,以便cast()静态方法可以工作。之所以FinalWrapper存在,是因为在 C++11 之前的版本中,我们无法对类型转换进行友元转换。

现在介绍参数化数据载体。除了需要参数化基类之外,它与下面的 C++11 变体相同。

// A string carrier
template <typename Base> class StringData : public Base {
    QString m_str;
public:
    explicit StringData(const QString & str) : m_str(str) {}
    QString value() const { return m_str; }
};
Run Code Online (Sandbox Code Playgroud)

C++11 元工厂

// The generic metafactory for unique event types that carry data
template <typename Derived, class Data> class EventMF {
    class Final;
    EventMF();
    EventMF(const EventMF &);
    ~EventMF();
public:
    class Event : public EventWrapper<Event>, public Data, private virtual Final {
    public:
        template<typename... Args>
        Event(Args&&... args): Data(std::forward<Args>(args)...) {}
    };
private:
    class Final {
        friend class Event;
    private:
        Final() {}
        Final(const Final &) {}
    };
};
Run Code Online (Sandbox Code Playgroud)

具有 Final 类的前向声明的体操是存在的,因为前向声明 Event 类需要更多的打字。

数据载体非常简单:

// A string carrier
class StringData {
    QString m_str;
public:
    explicit StringData(const QString & str) : m_str(str) {}
    QString value() const { return m_str; }
};
Run Code Online (Sandbox Code Playgroud)

使用与测试(通用代码)

现在我们可以使用通用元工厂来制作一些具体的元工厂,然后制作我们需要的事件类。我们创建两种携带数据的独特事件类型。这些事件类具有独特的staticType()s。

// A string event metafactory
template <typename Derived> class StringEventMF : public EventMF<Derived, StringData> {};

class Update : public EventMF<Update, StringData> {}; // using generic metafactory
class Clear : public StringEventMF<Clear> {}; // using specific metafactory
#if 0
// This should fail at compile time as such derivation would produce classes with
// duplicate event types. That's what the Final class was for in the matafactory.
class Error : public Update::Event { Error() : Update::Event("") {} };
#endif

int main(int, char**)
{
    // Test that it works as expected.
    Update::Event update("update");
    Clear::Event clear("clear");
    Q_ASSERT(Update::Event::staticType() != Clear::Event::staticType());
    Q_ASSERT(Update::Event::staticType() == Update::Event::cast(&update)->staticType());
    qDebug() << Update::Event::cast(&update)->value();
    Q_ASSERT(Update::Event::cast(&clear) == 0);
    qDebug() << Clear::Event::cast(&clear)->value();
    Q_ASSERT(Clear::Event::cast(&update) == 0);
}
Run Code Online (Sandbox Code Playgroud)