【问题标题】:Reasonable key types for map used to set properties of related classes?用于设置相关类属性的映射的合理键类型?
【发布时间】:2012-09-03 20:21:48
【问题描述】:

我打算编写一个代码,其中的类具有如下继承关系,并具有与材料类型相关的各种属性:

  1. 抽象基类Foo。没有与之关联的属性。
  2. Foo1GeneralElastic 继承自 Foo 类并具有与可能各向异性弹性材料相关的属性。
  3. Foo2GeneralElastic 也继承自 Foo 类,具有与 Foo1GeneralElastic 相同的材质属性,但在其他方面有所不同。
  4. Foo1PiezoElastic 继承自 Foo1GeneralElastic,具有压电特性和通用弹性特性。
  5. Foo1IsotropicElastic 继承自 Foo1GeneralElastic,但不共享其属性。

我决定抽象基类将具有一个或多个方法,这些方法采用MyPropMap 类型的映射,定义为:

typedef std::map<PropertyLabel,std::vector<double> > MyPropMap

对于 PropertyLabel 类型可能有几个不同的选项,我正在尝试权衡每种选项的优缺点:

让 PropertyLabel 成为 enum:这将是轻量级的,但它基本上是一袋标签,用于我正在考虑的每种材料的所有不同属性。

让 PropertyLabel 只是一个 int:在这里,我将为每种材料类型提供单独的头文件,每个文件都包含静态整数常量的定义,这些常量将是相关材料特性。例如,MatPropKeyGenElastic.hpp 将定义整数常量 ELASTICITY_MATRIXMatPropKeyIsotropicElastic.hpp 将定义常量 ELASTIC_MODULUSPOISSONS_RATIO,而 MatPropKeyPiezoElastic.hpp#include 文件 MatPropKeyGenElastic.hpp 并另外定义常量 @987654343 @。

棘手的事情是确保可以一起使用的所有常量都不会具有相同的值。这可以通过使用脚本生成头文件来实现,该脚本将这些常量的值设置为唯一值。

让 PropertyLabel 成为 std::string 从这里我可以采取几种不同的方式。我可以只在代码中使用像"ELASTICITY_MATRIX" 这样的字符串文字,并依赖这些文字永远不会拼写错误——一个会在运行时而不是编译时捕获的错误。我可以用类似于上述整数常量方案的方式定义字符串常量,保持常量唯一的任务很简单:只需将ELASTICITY_MATRIX 的值设置为"ELASTICITY_MATRIX",将POISSONS_RATIO 的值设置为@987654349 @等

除了额外的开销之外,我看到的问题是我看到了与非 POD 的全局静态常量相关的恐怖故事,例如主题 non-integral constantsDefining class string constants in C++? 中的 cmets .我想我可以将全局静态常量设置为 const char[] 数组,这些 POD 在用作映射键时会隐式转换为 std::strings(并且,no,我不打算让地图键本身为const char*)。我也可以使用预处理器定义字符串文字,但是我无法将它们保存在命名空间中。

您会推荐以上任何一种方法吗?里面有没有我没注意到的隐藏陷阱?您还有其他方法可以推荐吗?

【问题讨论】:

    标签: c++


    【解决方案1】:

    我不建议使用字符串。对于这么简单的任务来说太贵了。我投票给枚举。

    但是,如果将所有标签常量放在一个位置对您来说太难看,您可以制定更复杂的方法 - 使用组合键,如两个数字对 -(类 ID、属性 ID)。

    两者都可以定义为枚举,也可以是嵌套的。此外,类 ID 可以自动生成 - 例如在std::type_info 指针上使用reinterpret_cast 或仅使用std::type_info 指针或std::type_index(如果支持)。用代码说明想法:

    // PropertyLabel type, could be used as associative container key
    struct PropertyLabel: std::pair<const std::type_info*, int>
    {
      // Template ctor allows implicit conversion from enums
      // (actually not only from enums but from any int-compatible types)
      // Uncomment explicit keyword if implicit conversions scares you and use
      // explicit conversion syntax - PropertyLabel(smth).
      template <typename T> /*explicit*/ PropertyLabel(T label):
         std::pair<const std::type_info*, int>(&typeid(T), label)
      {
      }
    };
    
    // First property holder 
    class PropertyUser1
    {
    public:
      enum Labels
      {
         eProperty1,
         eProperty2,
         eProperty3,
      };
     };
    
    // Second property holder 
    class PropertyUser2
    {
    public:
      enum Labels
      {
         eProperty1,// Due to class scope you could use same names for different properties
         eProperty2,
         eProperty3,
      };
     };
    
    // Usage. A bit dangerous due to implicit conversions, but intuitive and handy:
    MyPropMap properties;
    properties[PropertyUser1::eProperty1].push_back(42.0);
    properties[PropertyUser2::eProperty1].push_back(42.42);
    // Will be with explicit ctor:
    // properties[PropertyLabel(PropertyUser1::eProperty1)].push_back(42.0);
    // properties[PropertyLabel(PropertyUser2::eProperty1)].push_back(42.42);
    

    看起来它可以通过更多的类型安全性来改进,消除使用非枚举类型(如int)的可能性,例如禁用像PropertyLabel(42) 这样的呼叫。但这只是为了说明想法。

    【讨论】:

    • 是我,还是继承自 std::pair 的 PropertyLabel 结构?
    • 我仔细查看了代码,并用它编译了一个测试程序。它对我有用,但取决于 typeid(PropertyUser1::Labels) 和 typeid(PropertyUser2::Labels) 的地址不同,因为在每个类中,eProperty1 具有相同的整数值。我会怀疑相信这种情况总是如此。由于 PropertyUser1::Labels 和 PropertyUser2::Labels 基本上都是整数,我可以看到另一个编译器的 typeid 的地址是 &typeid(int),然后代码会严重失败。
    • @jjramsey PropertyUserX::Labels 不是int,它是用户定义的枚举创建不同的类型,它应该引用不同的type_info 实例。但是,如果您不信任您的编译器,您可以为type_info 指针创建自己的包装器并使用operator==() (例如std::less 的'before()'),这保证为不同类型返回false,甚至枚举.或者,如果您的编译器支持,请使用 std::type_index
    • @jjramsey 正确,PropertyLabel 结构继承自 std::pair。只是为了不与自己的'operator
    【解决方案2】:

    我刚刚意识到一个相对简单的解决方案,它可以给我几乎我想要的东西,而不必大惊小怪。对于MyPropMap 类型的任何特定实例,我正在处理一种 特定类型材料的属性:各向同性弹性、压电、各向异性弹性等。鉴于此,我可以将每种材质类型对应的枚举包装在自己的命名空间中,并将它们放在适当的头文件中,例如,

    // MatPropKey/IsotropicElastic.hpp:
    namespace IsotropicElastic {
       enum { ELASTIC_MODULUS, POISSONS_RATIO };
    }
    
    // MatPropKey/GenElastic.hpp
    namespace GenElastic {
       enum { ELASTICITY_MATRIX }
    }
    
    // MatPropKey/PiezoElastic.hpp
    namespace PiezoElastic {
       enum { PIEZO_CONST_MATRIX, ELASTICITY_MATRIX }
    }
    

    这里有一些冗余,但我可以忍受。只要我坚持上述约定,那么在每个命名空间内,enum 值都是唯一的,并且只要我只在特定命名空间内为 MyPropMap 的每个实例使用 enum 值---无论如何我都想做——我很好。 (实际上,我还想将这些命名空间中的每一个包装在一个公共的MPKey 命名空间中。)当然,这不是万无一失的。例如,一个足够有创意的傻瓜可以决定同时使用#includeGenElastic.hppPiezoElastic.hpp,然后使用GenElastic::ELASTICITY_MATRIXPiezoElastic::PIEZO_CONST_MATRIX。坏事可能会发生。尽管如此,代码还是传达了命名常量应该如何分组,避免不必要的名称冲突是微不足道的。

    希望我早点想到。

    【讨论】:

      【解决方案3】:

      经过一番思考,我意识到了一些事情:

      1. 最好将地图包装在一个类中,这样我就可以更好地控制它的编写方式。
      2. 即使是封装的贴图也是通用的,并且必须能够适应任何材质参数类型,因此我只能提供这么多的编译类型安全性。

      鉴于此,我决定设计一个MatProp类,大致如下:

      #include <vector>
      #include <map>
      
      class MatProp {
      public:
         // Skipping the constructor details ...
      
         void setProp_Raw(int propId, double val);
         void getProp_Raw(int propId, double & val) const;
      
         void setProp_Raw(int propId, const std::vector<double> & vals);
         void getProp_Raw(int propId, std::vector<double> & vals) const;
      
         // More overloaded set/get funcs for complex scalars and vectors ...
      
      private:
         // The typedef allows me to write MatPropMap_::iterator, etc. in the
         // implementation of the member functions, which is handy if, say, 
         // I want to swap the std::map for an unordered_map later on.
         typedef std::map<PropertyLabel,std::vector<double> > MatPropMap_;
      
         MatPropMap_ matPropMap_;
      
      };
      

      set/get 函数以_Raw 为后缀,因为很容易输入错误的属性 ID 和值组合。我可以将信息传递给MatProp 的构造函数,以便可以在运行时验证这些函数的输入,但是设置它可能会变得笨重并使类更难使用。为了增加一些额外的安全性,我可以这样做,例如:

      void setIsotropicLinearElasticParameter(MatProps mProp,
                ElasPropEnum propId, // ELASTIC_MODULUS and POISSONS_RATIO are the 
                                     // *only* valid values of this parameter.
                double val) {
          mProp.setParam_Raw(propId, val);
      }
      

      函数很简单,但我明确声明(1)只允许两个键,(2)它们确实应该是double 类型。该界面并非完全万无一失,但正确使用它相当容易,并且使用错误需要一些努力。 FWIW,在这里做了类似的事情:http://blog.knatten.org/2010/04/23/make-apis-hard-to-use-incorrectly/

      【讨论】:

      • 想一想,我可能确实需要一些运行时安全性,但由于这是一个回答问题的论坛,而不是我对如何做事的猜测(可能会被修改)无论如何),我将保留我(两个!)对我自己的问题的答案。
      猜你喜欢
      • 2017-04-04
      • 1970-01-01
      • 2016-09-23
      • 1970-01-01
      • 2023-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多