关于c ++:在2018年使用C ++ 11及更高版本时,helper init()函数被认为是不好的形式吗?

In 2018 with C++11 and higher, are helper init() functions considered bad form?

前C ++ 11中没有非静态成员初始化,也没有构造委托,所以人们经常使用私有助手函数来帮助初始化,以减少代码复制。

这是2018年的好代码吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A  {
  int a1 = 0;
  double a2 = 0.0;
  string a3 ="";
  unique_ptr<DatabaseHandle> upDBHandle;

  void init(){
      upDBHandle = open_database(a1, a2, a3);
  }

public:
    A() { init(); }
    explicit A(int i):a1(i) {  init(); }
    explicit A(double d):a2(d) {  init(); }
    explicit A(std::string s):a3(std::move(s)) {  init(); }
    A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)) { init(); }
};

如何改进此代码?


在我看来,你的代码很好。我尽量避免依赖于一些细微的影响,比如构造函数初始化列表中的成员初始化顺序。它违反了dry-您需要重复使用相同的顺序:在类体中声明成员时,以及在构造函数初始化列表中。随着时间的推移,类变得更大,并且您将构造函数移动到.cpp文件中,事情就会变得更加混乱。因此,我将需要访问其他成员的内容放入init函数中。

如果成员是const,则不能这样做。但同样,作为类作者,您可以决定哪个成员是常量,哪个不是常量。请注意,不要将其与"construct,then init"的反模式混淆,因为这里init发生在构造函数中,而这对类用户是不可见的。

如果您仍然误导使用init,我建议不要将调用放入构造函数初始化列表。也许对我来说,一个可以接受的中间方法是将它放入类内初始值设定项中,并从构造函数中移除所有调用。

1
2
3
4
5
6
7
class A  {
  int a1 = 0;
  double a2 = 0.0;
  string a3 ="";
  unique_ptr<DatabaseHandle> upDBHandle = open_database(a1, a2, a3);

  // ...


可以使用这样一个事实:如果某个成员未在构造函数的成员初始化列表中初始化,则会执行默认的成员初始值设定项。此外,每个成员初始化都是一个完整的表达式,并且成员初始化总是按照类内声明的顺序执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A  {
  int a1 = 0;
  double a2 = 0.0;
  string a3 ="";
  unique_ptr<DatabaseHandle> upDBHandle = open_database(a1,a2,a3);
  //a1,a2 and a3 initializations are sequenced before
  //upDBHandle initialization.
public:
  //all these constructors will implicitly invoke upDBHandle's default initializer
  //after a1,a2 and a3 inialization has completed.
  A() { }
  explicit A(int i):a1(i) {  }
  explicit A(double d):a2(d) {  }
  A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)) { }
};


C++在处理多个缺省值方面不是很好。所以,做好这件事并不容易。您可以做不同的事情,但所有这些都有不同的权衡(例如分散违约)。

IMHO,最好的解决方案,可以在这里,是一个不是合法的C++(但),但是一个高度支持的扩展:指定的初始化器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A  {
  struct Params {
      int a1 = 0;
      double a2 = 0.0;
      string a3 ="";
  };
  Params p;
  unique_ptr<DatabaseHandle> upDBHandle;

public:
    explicit A(Params p_arg)
      : p(std::move(p_arg))
      , upDBHandle(open_database(p.a1, p.a2, p.a3) { }
};

A a({});  // uses all defaults
A a2({.a2 = 0.5});  // specifies a2 but leaves a1 and a3 at default
A a3({.a1 = 2, .a2=3.5, .a3 ="hello"});  //  specify all


好吧,这就是问题所在,在您的示例中,实际上根本不需要init。语言规则已经指定了默认成员初始值设定项go优先,然后指定在成员初始值设定项列表中执行的操作。成员按声明顺序初始化。所以你可以把每个任务定义为

1
2
3
4
5
A() upDBHandle(open_database(a1, a2, a3)) { }
explicit A(int i):a1(i), upDBHandle(open_database(a1, a2, a3)) {}
explicit A(double d):a2(d), upDBHandle(open_database(a1, a2, a3)) {}
explicit A(std::string s):a3(std::move(s)), upDBHandle(open_database(a1, a2, a3)) {}
A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)), upDBHandle(open_database(a1, a2, a3)) {}

就是这样。有些人可能会说这不好,因为更改类声明会使其中断。但是编译器非常擅长诊断这个问题。我属于思想学派,它认为程序员应该知道他们在做什么,而不仅仅是通过快乐的巧合来构建代码。

甚至可以在C++11之前完成。当然,您没有默认的成员初始值设定项,并且必须为每个成员重复默认值(可能首先命名它们,以避免出现幻数),但这与初始化依赖于其初始值的成员的问题无关。

init的优势?

  • 支持const成员,尽管他们可能很少出现。
  • 支持没有默认状态的对象。这些不能被默认初始化,直到调用init为止。
  • 无需向类中添加与函数无关的多余函数。

现在,如果成员的初始化代码并不简单,您仍然可以将其放入函数中。一个自由函数,希望它对类的翻译单元是静态的。像这样:

1
2
3
4
5
6
7
8
9
10
static std::unique_ptr<DatabaseHandle> init_handle(int a1, double a2, std::string const& a3) {
  // do other stuff that warrant a function block
  return open_database(a1, a2, a3);
}

A::A() upDBHandle(init_handle(a1, a2, a3)) { init(); }
A::A(int i):a1(i), upDBHandle(init_handle(a1, a2, a3)) {}
A::A(double d):a2(d), upDBHandle(init_handle(a1, a2, a3)) {}
A::A(std::string s):a3(std::move(s)), upDBHandle(init_handle(a1, a2, a3)) {}
A::A(int i, double d, std::string s) : a1(i), a2(d), a3(std::move(s)), upDBHandle(init_handle(a1, a2, a3)) {}

这也意味着您可以为不同的成员提供其中的几个。因此,现在初始化成员的问题更为广泛。

事实上,你可以通过弗雷德·拉森在他对你的帖子的评论中提出的一些建议来消除对很多人的需求。