代码之家  ›  专栏  ›  技术社区  ›  pmr

配置结构与设置器

c++
  •  5
  • pmr  · 技术社区  · 15 年前

    我最近遇到了一些类,它们使用配置对象而不是通常的配置设置器方法。一个小例子:

    class A {  
       int a, b;  
    public:  
       A(const AConfiguration& conf) { a = conf.a; b = conf.b; }  
    };  
    
    struct AConfiguration { int a, b; };
    

    好处是:

    • 您可以扩展您的对象并轻松地保证新值的合理默认值,而无需您的用户了解它。
    • 您可以检查配置的一致性(例如,您的类只允许一些值组合)
    • 通过调试setter可以节省大量代码。
    • 您得到一个默认的构造函数,用于为配置结构指定默认的构造函数,并使用 A(const AConfiguration& conf = AConfiguration()) .

    缺点:

    • 您需要在构建时了解配置,以后不能更改。

    我错过了更多的缺点吗?如果没有:为什么不经常使用?

    5 回复  |  直到 15 年前
        1
  •  6
  •   sbi    15 年前

    是单独传递数据还是按结构传递数据是一个样式问题,需要根据具体情况来决定。

    重要的问题是:对象在构造后是否准备好并可用,编译器是否强制您将所有必需的数据传递给构造函数,或者您是否必须记住在构造后调用一组setter,这些setter的数目可能随时增加,而编译器没有给您任何提示,您需要调整代码。所以不管这是不是

     A(const AConfiguration& conf) : a(conf.a), b(conf.b) {}
    

     A(int a_, int b_) : a(a_), b(b_) {}
    

    没那么重要。(有许多参数,每个人都喜欢前者,但这是哪个数字——以及这样的类是否设计得很好——是有争议的。)然而,我是否可以使用这样的对象

    A a1(Configuration(42,42));
    A a2 = Configuration(4711,4711);
    A a3(7,7);
    

    或者必须这样做

    A urgh;
    urgh.setA(13);
    urgh.setB(13);
    

    在我使用这个物体之前, 做一个巨大的改变。尤其是,当有人来添加另一个数据字段到 A .

        2
  •  4
  •   P Shved    15 年前

    使用此方法可以简化二进制兼容性。

    当库版本更改时,如果配置 struct 包含它,然后构造函数可以区分是传递“旧”配置还是传递“新”配置,并在访问非现有字段时避免“访问冲突”/“segfault”。

    此外,还保留了已损坏的构造函数名称,如果更改了其签名,该名称将发生更改。这也允许我们保持二进制兼容性。

    例子:

    //version 1
    struct AConfiguration { int version; int a; AConfiguration(): version(1) {} };
    //version 2
    struct AConfiguration { int version; int a, b; AConfiguration(): version(2) {} };
    
    class A {  
       A(const AConfiguration& conf) {
         switch (conf.version){
           case 1: a = conf.a; b = 0;  // No access violation for old callers!
           break;
           case 2: a = conf.a; b = conf.b;  // New callers do have b member
           break;
         }
       }  
    };  
    
        3
  •  2
  •   Rasmus Kaj    15 年前

    其主要优点是对象可以是不可变的。我不知道实际的配置结构是否比构造函数的A和B参数有任何好处。

        4
  •  1
  •   Matthew Murdoch    15 年前

    使用此方法会使二进制兼容更加困难。

    如果结构已更改(添加了一个新的可选字段),则使用该类的所有代码都可能需要重新编译。如果添加了一个新的非虚拟setter函数,则不需要重新编译。

        5
  •  0
  •   Matthieu M.    15 年前

    我支持这里降低的二进制兼容性。

    我看到的问题来自对结构字段的直接访问。

    struct AConfig1 { int a; int b; };
    struct AConfig2 { int a; std::map<int,int> b; }
    

    因为我修改了 b 我被搞砸了,但是:

    class AConfig1 { public: int getA() const; int getB() const;  /* */ };
    class AConfig2 { public: int getA() const; int getB(int key = 0) const; /* */ };
    

    对象的物理布局可能发生了更改,但getter没有更改,函数的偏移量也没有更改。

    当然,对于二进制兼容性,应该检查 PIMPL 成语。

    namespace details { class AConfigurationImpl; }
    
    class AConfiguration {
    public:
      int getA() const;
      int getB() const;
    private:
      AConfigurationImpl* m_impl;
    };
    

    当您最终编写更多的代码时,只要在现有的方法之后添加补充方法,就可以保证对象的向后兼容性。

    实例在内存中的表示不取决于方法的数量,它只取决于:

    • 虚拟方法的存在或不存在
    • 基本类
    • 属性

    它是可见的(而不是可访问的)。

    在这里,我们保证在属性上不会有任何变化。定义 AConfigurationImpl 可能会毫无问题地改变,方法的实现也可能改变。

    更多的代码意味着:构造器、复制构造器、赋值操作符和析构函数,这是相当数量的,当然还有getter和setter。还要注意,这些方法不能再内联,因为它们的实现是在源文件中定义的。

    不管它是否适合你,你自己决定。