配置结构体 vs 设置器

8

最近我发现有些类使用配置对象而不是通常的设置器方法进行配置。以下是一个小例子:

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

struct AConfiguration { int a, b; };

优点:

  • 您可以扩展对象并轻松地保证新值的合理默认值,而无需用户了解。
  • 您可以检查配置的一致性(例如,您的类仅允许某些值的组合)。
  • 通过省略设置器,您可以节省大量代码。
  • 您可以为指定Configuration结构的默认构造函数提供默认构造函数,并使用A(const AConfiguration& conf = AConfiguration())

缺点:

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

这种方法还有其他缺点吗?如果没有,为什么不经常使用它?

5个回答

7

无论是逐个传递数据还是按结构传递数据,这都是一种风格问题,需要根据具体情况进行决策。

重要的问题是:在构造之后,对象是否已准备好并可用,并且编译器是否强制您将所有必要的数据传递给构造函数,或者您是否需要记住在构造之后调用一堆设置器,而这些设置器的数量可能随时增加,而编译器又没有任何提示,告诉您需要调整代码。因此,无论如何,

 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添加另一个数据字段时。


4
使用这种方法可以更容易地实现二进制兼容性。
当库版本发生变化时,如果配置struct包含它,则构造函数可以区分传递的是“旧”还是“新”配置,并在访问不存在的字段时避免“访问冲突”/“段错误”。
此外,构造函数的名称也保留了其编码名称,如果更改了其签名,则名称将更改。这也使我们保持了二进制兼容性。
例如:
//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;
     }
   }  
};  

2

主要优点是A对象可以是不可变的。我不知道使用AConfiguration结构体是否比仅使用构造函数中的a和b参数更有优势。


1
使用这种方法会使二进制兼容性更加困难。
如果结构体被修改(添加了一个新的可选字段),所有使用该类的代码可能需要重新编译。如果添加了一个新的非虚拟 setter 函数,就不需要进行这样的重新编译。

我最近在支持一个旧的代码库时遇到了这个问题。真是相当烦人。 - Ben S
Windows API通过添加大小字段,确保结构体是POD,并仅在末尾附加数据字段来很好地完成了这项工作。添加支持旧构造函数以创建新的结构体(对于新字段具有合理的默认值),实际上增加了兼容性。 - sbi
我写了一个镜像答案,展示了兼容性的提高。 - P Shved

0

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

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

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; /* */ };

对象的物理布局可能已更改,但我的 getters 并没有改变,函数的偏移也没有改变。

当然,为了二进制兼容性,应该查看 PIMPL 习语。

namespace details { class AConfigurationImpl; }

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

虽然你最终会写更多的代码,但只要你在现有方法之后添加补充方法,你就可以保证对象向后兼容。

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

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

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

在这里,我们保证属性不会发生任何变化。AConfigurationImpl的定义可能会改变,方法的实现也可能会改变,但不会影响属性。

更多的代码意味着:构造函数、复制构造函数、赋值运算符和析构函数,这是相当多的,当然还有getter和setter。请注意,由于它们的实现是在源文件中定义的,因此这些方法不能再进行内联。

无论是否适合您,您都可以自行决定。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接