【问题标题】:How to replace this macro with inheritance, or template (or anything else?)如何用继承或模板(或其他任何东西?)替换这个宏
【发布时间】:2014-03-30 14:17:49
【问题描述】:

我的问题类似于:Can't use macro define class in C++,但稍微复杂一点:

class ABC
{
public:
    DECLARATION(ABC)

private:
    void ABCFun1();
    void ABCFun2();
    // ... and so on
}

#define DECLARATION(TYPE)\
    std::string GetClassName()\
    {\
        return std::string(#TYPE);
    }\
    // the macro can goes on to declare more
    // common interfaces, like Initialize(), ...etc.

所以,重点是,我可以使用这个宏来生成其他类,例如DEFGHI...等,
他们都共享DECLARATION的共同部分,但也有自己的private
零件。

如果不需要GetClassName(),似乎只能使用宏#来实现,
然后我可以使用继承将它们放在一起。
或者,如果没有私人的东西,我可以使用模板。

那么,把这两件事混在一起,有什么办法可以避免宏吗?
谢谢!

【问题讨论】:

  • 你能发布更多由宏生成的示例函数吗?

标签: c++ templates inheritance macros


【解决方案1】:

您可以使用CTRP 生成一些功能。但请记住,除非您使用宏,否则名称将不可用(当编译时反射成为标准时,可能会发生变化,可能适用于 C++17)。

CRTP

template<typename T>
class Base
{
public:
    void myFunc()
    {
        ((T*)this)->functionA();
        ((T*)this)->functionB();
        // ...
    }
};

class ABC : public Base<ABC>
{
    // ...
};

编辑:BЈовић 的回答是对的,但是一些关于 RTTI 的 cmets:

  • 它不便携
  • typeid(T).name() 依赖于实现(如果实现者愿意,它可以为每种类型返回 ""
  • 速度很慢。

请参阅this question about RTTI and LLVM,例如(或this one about performance)。

要存储字符串,您应该像现在一样使用预处理器。请注意,它不适用于模板化参数(即,如果您有template&lt;typename T&gt;,那么#T 将扩展为"T",而不是实际的类型名)。 然后,有一种技术允许将字符串传递给模板参数(排序):

#define NAME( _n_ , _t_ )             \
    struct _t_                        \
    {                                 \
        static const char* asName()   \
        {                             \
            return #_n_;              \
        }                             \
    }

它将字符串更改为一种类型。字符串本身是一个乱码,所以它被直接放入可执行文件中(并且是只读的,尝试修改它很可能会导致崩溃)。

我在 SPARK 粒子引擎中使用它,例如,我在其中实现了反射模块。

【讨论】:

  • 是的,我也不想使用typeid(T).name()
  • 有一天,这个名字将可以使用 C++ 标准反射(我不认为他们会为 C++14 做好准备,所以可能是 C++17)。
  • P.S.首先必须说谢谢,然后我需要一些时间来阅读它们!
  • 谢谢,我已阅读您的回答。 CRTP 很棒,很适合我的需要。太糟糕了,知道字符串转换的唯一方法仍然是宏,但我想我可以接受#x 的最小使用来获取字符串。
【解决方案2】:

据我从模糊的“宏继续...”评论中可以看出,DECLARATION 宏似乎做了以下一件或几件事情:

  1. 定义成员独立于ABC
  2. 定义依赖于类型的成员(方法或数据成员)ABC
  3. 定义使用字符串的成员"ABC"

tl;dr:正如您在问题中指出的那样,#1 是微不足道的。 #2 比较简单,使用 CRTP,#3 没有宏就不能令人满意。

更新:

在下面的 cmets 中,您提到您希望保证字符串和类名是“同步的”。如果没有当前 C++ 中的宏,这绝对是不可能的。 (参见下面的“更新”)

第一个很容易在没有宏的情况下实现 - 只需从定义成员的普通基类继承即可。

第二个也可以相对简单地完成,通过 CRTP:创建一个模板类,将类型作为参数并从模板继承,并使用类型本身进行实例化:

template <class T>
class Base {
public:
  void doSomething(T const&);
};

class ABC : public Base<ABC> {
  // inherited void doSomething(ABC const&);
};

第三个很棘手,如果没有至少一点样板文件就不容易解决。

除了将名称(类名、函数名、变量名...)转换为代表该名称的字符串的宏之外,C++ 没有其他功能。 (这种语言特征是通常称为反射的一系列特征的一部分)。 typeid(T).name() 是一个例外,但结果不是标准化的,所以它可能会或可能不会给你类的名称,不同的东西,可读或不可读或只是一个空字符串。

因此,要(可移植且可靠地)为类ABC 获取字符串“ABC”,除了类定义之外,您必须至少编写一次“ABC”。这还不算太糟糕,您也必须为#2 做类似的事情,因为它是seems impossible to use the type of ABC without explicitly mentioning ABC again

更新:由于您必须显式键入字符串类名,因此无法保证两者始终相同,除非使用宏。即使您在问题中拥有的宏也容易发生更改/错别字:

class ABC {
  DECLARATION(ACB);
};

如果宏只提供它的参数的字符串化版本而不对类型做任何事情,这将给出一个错误的类名(在这种情况下,编译器会告诉你 ACB 不是类型)。如果您想要保证,请使用一个可以做所有事情的宏,包括类定义:

#define CLASS_DEF(x)           \
class x##_base {               \
  std::string GetClassName() { \
    return std::string(#x);    \
  }                            \
};                             \
class x : public x##_base

然后:

CLASS_DEF(ABC)
{
private:
    void ABCFun1();
    void ABCFun2();
    // ... and so on
}

将它放在基类中可以编写宏,然后只是一个普通的类体。

如果我们不需要同步保证,那么如果我们不需要宏,我们可以为#3 做些什么呢?

  • 将字符串作为模板参数提供给某个基类将是最好的方法,但简而言之,这是不可能的。 This article 将字符串作为模板参数,最后不得不退回到宏。我们能得到的最接近的是文章中提到的某种形式的boost::mpl::string&lt;'Hell','o Wo','rld!'&gt;。如果 C++11 可变参数模板不可用,长度将受到一些任意定义的限制,并且基类(或boost::mpl::string)的定义本身可能会使用大量的宏魔法,但至少你是摆脱类本身中的自定义宏。使用 #2 #3 的东西必须看起来有点像(我将名称加长以说明该方法的限制)

    class ABCDE: public Base<ABCDE, 'ABCD', 'E'> { /* ... */ }
    
  • 将字符串作为具有预定义名称的静态数据成员常量。这是一个非常适合与 CRTP 一起使用的选项,如果您无论如何都必须使用它:

    template <class T>
    class Base {
    public:
      std::string GetClassName() const {
        return T::name;
      };
    };
    
    class ABC : public Base<ABC> {
    public:
      constexpr static char const *  name = "ABC";
    };
    

    在 C++03 中,您必须使用必须在类定义之外定义的 static const 成员(即在 ABC.cpp 中)

  • 将字符串作为 ABC 的非静态数据成员的工作方式类似:

    template <class T>
    class Base {
    public:
      std::string GetClassName() const {
        return static_cast<T const*>(this)->name;
      };
    };
    
    class ABC : public Base<ABC> {
    public:
      const std::string name = "ABC";
    };
    

    这需要 C++11 进行类内成员初始化。在 C++03 中,名称必须在每个构造函数中正确初始化。

  • 将字符串作为基类的数据成员有效,并且应该是 imo 的首选,如果您没有类型相关的成员,即如果您不需要 CRTP 来执行 #2 :

    class Base {
    public:
      Base(std::string nm) : name(nm) {}
      std::string GetClassName() const {
        return name;
      };
      std::string name;
    };
    
    class ABC : public Base {
    public:
      ABC() : Base("ABC") {}
      ABC(int i) : ABC() { /*another ctor*/ }
    };
    

    只需使用 C++11 委托构造函数就可以使用类名初始化 Base,尽管在每个初始化列表中写入 Base("ABC")ABC() 并没有太大区别。

【讨论】:

  • 不错的答案。在我的回答中,我有一种方法可以传递您没有涵盖的字符串(尽管您的回答非常彻底)
  • @Synxis 谢谢。据我所知,您传递字符串的方法是您使用的 NAME 宏,这是我想避免的,因为 OP 询问了避免使用宏。
  • 谢谢你的回答,我接受Synxis的回答,因为他之前发的,希望你不要介意。此外,你提到的CLASS_DEF(ABC)是我要避免的,因为我不想在宏中隐藏真正的代码,但是CRTP技术仍然是我想要的,谢谢!
【解决方案3】:

如果只需要获取类型的名称,可以使用typeinfo::name()方法。

您可以使用 CRTP,它包含您需要的一切。像这样:

template< typename T >
class MyBase
{
  public:
    MyBase() : className( typeid(T).name() ){}
    virtual ~MyBase(){}


    std::string className;
    // common interfaces, like Initialize()
};

然后使用它:

class A : public MyBase<A>
{
};

【讨论】:

  • 确实可以使用RTTI来获取类型的名称,但不方便的是对性能有影响(取决于实现),而且不是真的很便携。在这里,如果Tint,gcc 将给出名称i,而不是int
  • @Synxis 该名称仍然是唯一的。此外,性能不是问题,除非您每秒创建成千上万个对象。
  • 其实名字并不要求是唯一的。例如,请参阅here
  • @Synxis 对。尽管它是实现定义的,但我还没有看到返回非唯一名称的实现。据我所知,所有支持 c++11 的现代主流编译器都返回了唯一的字符串。
猜你喜欢
  • 1970-01-01
  • 2019-07-20
  • 1970-01-01
  • 1970-01-01
  • 2021-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-09
相关资源
最近更新 更多