【问题标题】:D.R.Y vs "avoid macros"D.R.Y 与“避免宏”
【发布时间】:2009-10-12 14:28:25
【问题描述】:

我正在使用 Windows API 在 C++ 中创建自己的 XUL 实现。元素由 XML 解析器构造的事实要求它们具有相同的接口,这样我们就不需要为每个元素构造函数编写自定义代码。结果是我的大部分元素看起来像这样:

class Button : public Element
{
public:
    static const char * Type() { return "button"; }

private:
    friend class Element;
    Button(Element * inParent, const AttributesMapping & inAttributesMapping);
};


class Label : public Element
{
public:
    static const char * Type() { return "label"; }

private:
    friend class Element;
    Label(Element * inParent, const AttributesMapping & inAttributesMapping);
};


class Description : public Element
{
public:
    static const char * Type() { return "description"; }

    virtual bool init();

private:
    friend class Element;
    Description(Element * inParent, const AttributesMapping & inAttributesMapping);
};

所以这里有很多代码重复。我想知道用这样的宏调用替换它们是否是个好主意:

#define DECLARE_ELEMENT(ElementType, XULName)           \
class ElementType : public Element                      \
{                                                       \
public:                                                 \
    static const char * Type() { return XULName; }      \
                                                        \
private:                                                \
    friend class Element;                               \
    ElementType(                                        \
        Element * inParent,                             \
        const AttributesMapping & inAttributesMapping); \
};                                                      \


DECLARE_ELEMENT(Window, "window")
DECLARE_ELEMENT(Button, "button")
DECLARE_ELEMENT(Label, "label")

我还没有完全弄清楚这个概念,所以这里缺少一些东西,比如类定义,以及(可能)为每个元素添加方法的能力。

但我想知道您对在这种情况下使用宏的看法。欢迎分享您的想法。

编辑

我现在正在使用一个小的 ruby​​ 脚本,它从一组模板生成源文件和头文件。我增强了脚本,以便文件也自动标记为在 SVN 上添加,并且修改了 Visual Studio 项目文件以包含这些文件。这为我节省了大量的体力劳动。我对这个解决方案很满意。仅供参考,这就是模板现在的样子:

#ifndef {{ELEMENT_NAME_UPPER}}_H_INCLUDED
#define {{ELEMENT_NAME_UPPER}}_H_INCLUDED


#include "XULWin/Element.h"


namespace XULWin
{

    class {{ELEMENT_NAME}} : public Element
    {
    public:
        static ElementPtr Create(Element * inParent, const AttributesMapping & inAttr)
        { return Element::Create<{{ELEMENT_NAME}}>(inParent, inAttr); }

        static const char * Type() { return "{{ELEMENT_TYPE}}"; }

        virtual bool init();

    private:
        friend class Element;
        {{ELEMENT_NAME}}(Element * inParent, const AttributesMapping & inAttributesMapping);
    };

} // namespace XULWin


#endif // {{ELEMENT_NAME_UPPER}}_H_INCLUDED

CPP 文件:

#include "XULWin/{{ELEMENT_NAME}}.h"
#include "XULWin/{{ELEMENT_NAME}}Impl.h"
#include "XULWin/AttributeController.h"
#include "XULWin/Decorator.h"


namespace XULWin
{

    {{ELEMENT_NAME}}::{{ELEMENT_NAME}}(Element * inParent, const AttributesMapping & inAttributesMapping) :
        Element({{ELEMENT_NAME}}::Type(),
                inParent,
                new {{ELEMENT_NAME}}Impl(inParent->impl(), inAttributesMapping))
    {
    }


    bool {{ELEMENT_NAME}}::init()
    {
        return Element::init();
    }

} // namespace XULWin

【问题讨论】:

  • 我支持 DRY。让事情变得更清洁。
  • 我使用宏主要是为了避免 symbol 在一行或两行中重复。像您这样的#define 墙通常可以解析为辅助函数/模板,有时还可以解析为简单的声明宏。 MartinB 为您的案例展示了一个很好的解决方案,甚至不需要宏。
  • 那你为什么不做模板版本呢?
  • @GMan,我意识到宏和模板不起作用的原因与不同子类中可能存在实现差异的原因相同。例如,一些 Element 子类型需要实现 init 方法。我编写的脚本提供了核心实现,之后我可以进行任何需要的修改。

标签: c++ dry


【解决方案1】:

如果你使用模板解决方案,你可以避免使用宏并且避免重复自己:

template <const char *XULName>
class ElementType : public Element
{
public:
    static const char * Type() { return XULName; }

private:
    friend class Element;
    ElementType(
        Element * inParent,
        const AttributesMapping & inAttributesMapping);
};

char windowStr[]="window";
char buttonStr[]="button";
char labelStr[]="label";

typedef ElementType<windowStr> Window;
typedef ElementType<buttonStr> Button;
typedef ElementType<labelStr> Label;

经验法则:模板几乎可用于 C 语言中需要宏的所有内容。

实现说明:字符串文字不能直接用作模板参数,因为它们具有内部链接——这就是为什么你需要windowStr 等。在实践中,你可能希望将windowStr、@ 的声明H 文件中的 987654324@ 和 labelStr 以及 CPP 文件中这些字符串的定义。

【讨论】:

  • +1,我知道不能使用文字字符串作为模板参数,但这是我第一次看到这种解决方法。不确定我是否愿意使用它,但拥有更多工具来完成这项工作总是好的!
  • @Matthieu:同意...我也觉得这个解决方法有点不确定。一个更令人满意的解决方案是使用特征类......在这种情况下,迟早会需要它。
【解决方案2】:

我不会在这里使用宏。线索在你的类“描述”中,它有一个额外的成员函数init,而其他人没有。因此,您将无法使用宏来定义它,而是手动扩展宏并添加额外的行。

对我来说,这比写出所有的类定义更违反 DRY。 几乎不是重复自己,而是只为一种情况而这样做,通常最终很难保持一贯的重复自己。 DRY 是关于找到好的抽象,而不仅仅是减少样板。

不过,我可能会用 Element 类中的 SetAttributes 函数替换这些构造函数。这可能会减少每个派生类中实际需要的样板数量,因为构造函数是不能从基类继承的东西。但这取决于每个类的构造函数的实现有多相似。

【讨论】:

    【解决方案3】:

    作为替代方案,您可以考虑在单独的构建步骤中生成代码,而不是使用预处理器。我喜欢cog,但是你可以使用任何你喜欢的东西——这样你就可以完全以编程方式控制生成的内容。 (宏功能强大,但功能有限。)

    【讨论】:

    • 我很惊讶没有其他人看到第三个选项。 ++ 用于代码生成。您可以在需要的地方添加自定义零件。您可以生成适当的文档。您不会像宏那样中断行编号/调试。如果您无法为此解决方案使用 c++ 模板,请使用代码生成。
    【解决方案4】:

    我认为宏可以在这样的低级别减少重复(从而减少引入错误的风险)。

    宏的使用将保持非常本地化,并且应该使整个代码更易于理解。当然,它也可能需要一些文档工作。

    【讨论】:

    • 我完全同意。避免宏并不意味着完全禁止它们。在这种情况下,您的代码通过宏变得更加清晰,并且通过 DRY 更不容易出错。
    【解决方案5】:

    使用任何使代码更简单的东西。

    DRY 和避免宏都有相同的目标:让您的代码更简单。

    • DRY:避免重复
    • 避免使用宏:因为它们可能会引入难以诊断的编译器错误或难以诊断的错误(因为它们绕过命名空间边界并且不支持 C++/类型安全)。

    像往常一样,我建议遵循精神而不是文字。在您的情况下,很明显该宏实际上会简化您的代码,因此您可能应该使用它。

    但是,考虑到宏可能引入的问题,请确保将其命名为“安全”。例如,在开头包含项目名称/文件名以减少与现有宏的潜在“冲突”。

    (您可以查看 BOOST 标头保护以了解命名约定)

    【讨论】:

      【解决方案6】:

      如果您打算使用 doxygen 等自动代码文档工具,请谨慎使用替换 class 定义的宏。在生成任何文档之前,您必须通过预处理器运行代码。也许不是最重要的考虑因素,但仍然需要考虑。

      【讨论】:

      • 感谢您的提示!到目前为止我还没有想到。
      • 我正在处理一些遗留代码,这些代码使用宏来替换所有类和函数的声明和定义。使处理程序结构变得困难,因为它混淆了 IDE 和文档生成器。
      【解决方案7】:

      恕我直言,这个宏是合理的。虽然我认为添加#undef DECLARE_ELEMENT 以防止悬空宏会更好。 (除非您也打算在其他文件中使用此宏。)

      但是请注意,这只有在这些类永远不会有太大差异(或完全不同)时才有效。


      还有另一种使用模板的解决方案。考虑以下代码

      namespace impl
      {
          struct ButtonTag;
          struct LabelTag;
      
      
          template< typename TypeTag >
          struct NameGenerator;
      
          template<>
          struct NameGenerator< ButtonTag >
          {
              static const char * getName() { return "button"; }
          };
      
          template<>
          struct NameGenerator< LabelTag >
          {
              static const char * getName() { return "label"; }
          };
      
      
          template< typename TypeTag >
          class SimpleElement : public Element
          {
          public:
              static const char * Type()
              { return NameGenerator< TagType >::getName(); }
      
          private:
              friend class Element;
      
              SimpleElement(
                  Element * inParent,
                  const AttributesMapping & inAttributesMapping);
      
          };
      }
      
      typedef impl::SimpleElement< impl::ButtonTag > Button;
      typedef impl::SimpleElement< impl::LabelTag > Label;
      

      它有点冗长,但避免使用宏。

      【讨论】:

        【解决方案8】:

        代码示例

        enum Types { BUTTON, LABEL,...}
        
        struct TypeList {
            static const char * Type(const int nID)
            {
                 switch(nID) {
                 case BUTTON: return "button";
                 ...
            }
        };
        
        template<ID>
        class IElem : public Element
        {
        private:
            static TypeList m_oTypeList;
        
        public:
            static const char * Type() { return m_oTypeList.Type(ID); }
        private:
        friend class Element;
            IElem(Element * inParent, const AttributesMapping & inAttributesMapping)
            {...}
        };
        

        用于非常用功能和专用

        class Button : public IElem<BUTTON>
        {
        ...
        }
        

        【讨论】:

          【解决方案9】:

          我什至可以更进一步,在使用宏时同时使用单散列和双散列功能。单哈希创建字符串常量和双连接标识符以构建新的组合。

          #define DECLARE_ELEMENT(ElementType)                     \
          class C ## ElementType : public Element                  \
          {                                                        \
          public:                                                  \
              static const char * Type() { return # ElementType; } \
                                                                   \
          private:                                                 \
              friend class Element;                                \
              C ## ElementType(                                    \
                  Element * inParent,                              \
                  const AttributesMapping & inAttributesMapping);  \
          }
          
          DECLARE_ELEMENT(window); // defines Cwindow
          DECLARE_ELEMENT(button); // defines Cbutton
          DECLARE_ELEMENT(label);  // defines Clabel
          

          例如,我有时会编写以下代码来测试某些常见类型的 sizeof。

          #include <stdio.h>
          
          #define OUT( _type ) printf("sizeof(%s) = %d\n", #_type, sizeof(_type))
          
          int main() {
            OUT( char );
            OUT( int );
            OUT( short );
            OUT( long );
            OUT( long long );
            OUT( void* );
            return 0;
          }
          

          【讨论】:

            【解决方案10】:

            在这种情况下,我会投票支持宏。毕竟它们并没有那么糟糕,你不应该尝试用它们编写内联函数,但除此之外它们很好。

            【讨论】:

              【解决方案11】:

              我认为在这种情况下使用宏是可以的,但前提是你

              • 可以开发一个不太复杂但涵盖(最好)所有必要的Element 类结构的解决方案
              • 很好地记录宏
              • 意识到某些 IDE 在宏生成的类结构方面存在问题,并且可以承受后果

              【讨论】:

                【解决方案12】:

                这段代码看起来非常像 Tom Cargill 在他的书“C++ Programming Style”的第 1 章中剖析和重组的程序,该程序可以追溯到 1992 年。诚然,该代码没有使用宏来复制几乎相同的类,但最终结果看起来非常相似。

                【讨论】:

                • @StackedCrooked:来自图书馆——是的;购买 - 不一定。最近有更多的书籍更好地涵盖了现代 C++,但这仍然涵盖了大部分基础知识,而且在我看来,它并没有完全过时。你仍然可以从亚马逊购买新的——这让我很惊喜(我看的时候他们还剩下一个)。
                • 本章的要点是不要构建仅返回值不同的类。您的宏方案不正确(或者,也许更仁慈或更准确地,足够)允许派生类的变化。粗略地说,如果你可以在宏中处理这一切,那么类之间就没有足够的差异来保证使用不同的类——它们应该通过更通用类的参数化(按值)来处理。这大概是 20 页的快速注释——尽管每一页上的材料并不是那么密集(它是一本开放、易于阅读的书)。
                • 你是对的,宏方案不允许派生类的变化。这就是我放弃这个想法的原因。
                猜你喜欢
                • 2023-03-07
                • 2010-12-25
                • 1970-01-01
                • 1970-01-01
                • 2012-04-03
                • 2022-01-17
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多