【问题标题】:How to declare a member in a base template class where the type is dependent of the derived class?如何在类型依赖于派生类的基模板类中声明成员?
【发布时间】:2017-12-10 20:52:14
【问题描述】:

给定一个使用 CRTP 的基类,我正在考虑在基模板类中声明一个成员,其中类型依赖于派生类。

虽然以下工作按预期工作:

template <class T> class BaseTraits;
template <class T> class Base {
    using TypeId = typename BaseTraits<T>::TypeId;
    TypeId id;
 public:
    Base() { id = 123; }
    TypeId getId() { return id; }
};

class Derived;
template <> class BaseTraits<Derived> {
public:
    using TypeId = int;
};

class Derived : public Base<Derived> {};

int main(int argc, char ** argv) {
     Derived foo;
     return foo.getId();
}

我想知道我是否可以简化实现。我可以向Base 模板添加第二个模板参数,并使BaseTraits 更简单,甚至去掉它。然而,上面的 sn-p 已经是尝试删除第二个模板参数了。我正在寻找不涉及 Base 的第二个模板参数的解决方案。

我尝试了类似以下的方法,但它无法编译:

错误:对不完整类型“类派生”的无效使用

template <class T> class Base {
    using TypeId = typename T::TypeId;
    TypeId id;
 public:
    Base() { id = 123; }
    TypeId getId() { return id; }
};

class Derived : public Base<Derived> {
public:
    using TypeId = int;
};

int main(int argc, char ** argv) {
     Derived foo;
     return foo.getId();
}

更新:

  • 我仅限于 c++14。
  • Base 必须是模板。
  • 性能是必须的。

【问题讨论】:

  • 好吧,您的第二个示例不起作用,因为在完全声明之前,您不能将类用作模板参数。你想通过这些课程实现什么?您确定它们需要模板化吗?
  • @MarošBeťko 你非常可以,此时它是一个不完整的类,你可以使用一个不完整的类作为模板参数。
  • @MarošBeťko 我希望 Base 声明一个只有 Derived 知道类型的成员。是的,我需要使用模板,至少对于 Base。
  • 您真的需要id 成为Base 的成员吗?你也可以让它成为Derived 的成员......
  • 不幸的是,是的。事实上,我需要一个数组。

标签: c++ c++11 templates crtp abstract-base-class


【解决方案1】:

是否可以让成员类型直接依赖于派生类?将 auto 声明的成员函数的结果类型(推导出的返回类型),不可能。

因此,type-trait 的使用就像您在 您的解决方案中所做的那样是最好的也是唯一的解决方案

原因是基类在定义派生类时必须是一个完整的类型:编译器在解析派生类定义之前必须先实例化和解析基类定义,C++ standard N4140 [derived.class]/2(粗体是我的):

由基本类型说明符表示的类型应该是一个不是未完全定义的类的类类型;[...]

【讨论】:

  • 你有没有注意到这个毫无意义的双重否定“not an incompletely”!
【解决方案2】:

这样的事情怎么样:

template <typename T, typename TypeId> class Base 
{
private:
    TypeId id;
public:
    Base() { id = 123; }
    TypeId getId() {return id;}
};

class Derived : public Base<Derived, int> {};

【讨论】:

  • 谢谢@killzone-kid。是的,它会更简单,但是我正在寻找不涉及 Base 的第二个模板参数的解决方案。事实上,这就是我今天所拥有的。我的目标是将 Base 的模板参数数量减少到一个。
  • 你可以看到这里没有使用第一个参数,我留下它是因为你最初拥有它,我认为你以后可能需要它。如果你愿意,你可以template &lt;typename TypeId&gt; class Baseclass Derived : public Base&lt;int&gt; {};
  • 我提取了真实代码的要点来关注这个问题。 Derived 需要从 Base 继承。
【解决方案3】:

这有点简化,但你要为此付出一些代价。

#include <any>

template <class T> class Base {
    std::any id; // expensive, but cannot have T::TypeId here
 public:
    Base() : id(123) {}
    auto getId() { 
         return std::any_cast<typename T::TypeId>(id); 
    } // T::TypeId is OK inside a member function
};

class Derived : public Base<Derived> {
public:
    using TypeId = int;
};

【讨论】:

  • 谢谢@n.m。您的解决方案很好,但是我不能使用 std::any 因为我在当前环境中仅限于 c++14,这是一个性能关键的应用程序。
  • 您可以编写自己的简化版any 或使用boost::any(或在紧要关头使用void*),但无论如何性能都会受到影响。
【解决方案4】:

为什么不颠倒类层次结构?

template <class T>
class Base : T {
    using TypeId = typename T::TypeId;
    TypeId id;
 public:
    Base() { id = 123; }
    TypeId getId() { return id; }
};

struct BasicDerived {
    using TypeId = int;
};


using Derived = Base<BasicDerived>;

【讨论】:

  • 我不确定是否会跟随。 Derived 需要是使用 CRTP 的 Base 的子类。
  • @Flyer 是的,它不再是 CRTP。 Derived 现在是基类,Base 现在是派生类。
  • 我有多个派生类。 Base 必须保持父类。不过这是一个有趣的想法:)
  • @Flyer 如果你需要 Base 来保持父类,这确实不适用。
  • @Flyer 我建议您查看 cppcon 2017 演讲“基于代理的类设计”。它是关于组合类的接口和实现。
【解决方案5】:

其实,我想多了……这还不算太不愉快:
你可以有一个绑定结构,甚至可以写成一个宏,在真正的类之前声明。
绑定结构为真实类定义了枚举和不完整的 typedef。
模板是在所有这些之前定义的,但是使用 typename 来延迟它的依赖,但是它是由真实类实例化的,并且只依赖于绑定结构

template <class ThatClassWrapper>
class MyBase
{
protected:
    typedef typename ThatClassWrapper::TypeId TypeId;
    typedef typename ThatClassWrapper::RealClass ThatClass;
    TypeId typeIdValue;
    TypeId  GetTypeId() {   return typeIdValue; }
    std::vector<ThatClass*> storage;
};

class SomeClass;
namespace TypeIdBinding
{
    struct SomeClass
    {
        enum TypeId
        {
            hello, world
        };
        typedef ::SomeClass RealClass;
    };
}
class SomeClass: public MyBase<TypeIdBinding::SomeClass>
{
public:
    bool CheckValue(TypeId id)
    {   return id == typeIdValue;   }
};

请注意,真正的类使用模板库中定义的 TypeId,并且命名成员不直接可见。您可以通过让模板 Base 从绑定结构派生来解决这个问题(确认它以这种方式编译)。尽管我实际上喜欢在 c++11 中这样做,但您可以仅从另一个命名空间导出或 typedef 枚举类型名,并将该类型名用作枚举成员的前缀,有助于避免名称污染。

【讨论】:

  • 感谢您的其他建议。你很有创意 :) 我同意它并不太令人不快,但它不再是 CRTP 并且它失去了 Derived > Base 继承。正如你所说,我似乎遇到了循环依赖的墙,我认为@oliv 是对的。
【解决方案6】:

说实话,你已经碰到了硬循环依赖的墙。 任何出路都会很臭。
最终,两个模板参数似乎是一个很小的代价。

你能声明一个接受 Derived 和 TypeID 的虚拟模板类吗?不过,我认为这对你没有任何好处。

TypeID:Derived 是 1:1 映射吗?用另一个帮助模板过度表示 1:1 映射到从 TypeID 派生的回溯查找会更好吗?请注意,需要在 Derived 类之外定义 TypeID 才能执行此操作。
TypeID 真的需要在类中定义吗?它是否可以过滤掉 Base 中传入的定义以支持内部 typedef 的现有使用?

你能双重包含吗?拆分或修改派生的定义,以便 typeid 位于可以包含在模板之前的基类定义中?这个 DerivedBase 可以在命名空间中声明,并包含指向完整 Derived 类的 typedef 链接,因此 Base 可以找到它以供参考。

【讨论】:

  • 感谢您的建议。事实上,我可能会保留第二个模板参数。 “TypeID:Derived 是 1:1 映射吗?”是的“使用另一个帮助模板过度表示 1:1 映射到从 TypeID 派生的回溯查找会更好吗?”你的意思是像 BaseTraits 模板一样吗?我不知道。它似乎并不比第二个模板参数简单得多。 “TypeID 真的需要在类中定义吗?”不,我不明白后续问题。 “双重包含还是拆分?”那很有意思。你说的 typedef link back 是什么意思?
猜你喜欢
  • 2021-07-25
  • 1970-01-01
  • 1970-01-01
  • 2012-09-12
  • 2018-04-26
  • 2013-02-18
  • 2013-12-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多