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(); }
}; |
如何改进此代码?
- 不,不是。甚至在C++ 11之前,EDCOX1(0)就可以轻松地返回一个完全构造的句柄来初始化成员句柄。
- 这是可怕的前和后C++ 11。
- 我相信这个问题最好在代码审查时问。
- @讲故事的人,我不确定这是否像你说的那样可怕。init函数最糟糕的事情是当它们是公共的并且您有两个阶段的初始化。这里不是这样的。如果您希望基于a1、a2和a3的值初始化多个成员变量,那么更好的解决方案是什么样子的?C++没有处理多个默认值的好方法,而笨拙的解决方案反映了这一点。
- 这可能是命名参数习语的一个很好的候选者。
- @nirfredman-几个自由的init_1到init_n函数,这些函数最好甚至不在A的头文件中。对此,任何借口都只是一个借口。
- @Nirfredman为此示例委托构造函数。如果委托的构造函数不合适,那么我可能会考虑工厂/生成器。
- @说书人嗯什么?我不知道你的意思。也许你应该回答,然后我们会看到?如果你说的是免费的工厂功能,那么…
- @0x5453委托构造函数并不能很好地解决这个问题,因为您必须重复默认值。工厂函数也是如此;工厂函数只取一个字符串,而工厂函数只取两个字符串,都需要访问int的默认值。
- 使用委托构造函数示例。godbolt.org/g/jvnlw公司
- @好吧,你写的构造器和OP不一样。如果您编写了一个只接受双精度和字符串的构造函数,那么您将违背我上面所写的:重复int的默认值。
- 字符串a3="是无用的初始化,因为字符串将由其默认ctor为空。这行没必要。
在我看来,你的代码很好。我尽量避免依赖于一些细微的影响,比如构造函数初始化列表中的成员初始化顺序。它违反了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);
// ... |
- 撇开我们对这件事的不同意见,你最后的建议是无可救药地被打破了。讽刺的是,它会因为那些你不喜欢依赖的微妙之处而崩溃。
- @讲故事的人这里没有微妙之处。不违反干燥规则。您可以一直向下移动upDBHandle,使其成为最后一个成员,而不必依赖其在其他任何地方作为最后一个成员的位置。
- 不是我的观点。您依赖于按特定顺序初始化的成员。成员初始值设定项是否列出,它仍然依赖于它。或者这不是你所说的"微妙"效果吗?mem初始值设定项列表中成员的顺序不重要。
- @讲故事的人我提到的"精妙之处"是忽略了构造函数初始化列表中的顺序,而使用声明顺序,声明顺序在程序中处于不同的位置。我不能代表其他人说话,但对我来说,我的代码片段和int main() { int a = 1; return a + 2; }的代码片段在细微之处相似,尽管有点微妙。因此对我来说,这只是一个"可接受的中途",而不是我最喜欢的解决方案。
- 我建议你把那一点磨尖。""两个地方"还不够具体。我认为这是指不同的人。
- @讲故事的人,我做了更多的阐述。
- +1自动地告诉OP他的代码是好的,我为OP感到遗憾的是,最乐观的事情告诉他他的代码是可怕的,即使在C++中给出更好的解决方案并不容易!可能也值得一提的是-wreorder警告大多数(全部?)主要编译器支持;对于不了解此标志的人来说,此标志对于避免您提到的一些微妙之处是一个巨大的帮助。
- @约翰内斯感谢你的职位。这是一个艰难的决定,该由谁来决定分数。
可以使用这样一个事实:如果某个成员未在构造函数的成员初始化列表中初始化,则会执行默认的成员初始值设定项。此外,每个成员初始化都是一个完整的表达式,并且成员初始化总是按照类内声明的顺序执行:
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 |
- 型投反对票的原因?
- 型可能是因为它没有解决问题的核心问题,upDBHandle。
- 型@故事讲述者整个问题是由有多个构造器引起的,有多个构造器是由于需要不同的默认组合…此代码准确地解决了原始问题的用例。
- 型如果你这样说。。。
- 型@讲故事的人不知道从中得到什么。它避免了成员初始化和默认值的重复,并允许用户使用所需的任何默认值和覆盖组合构造一个A。这不是最初问题的目标吗?如果用户不关心默认值,那么他们可以定义一个构造函数来获取所有三个参数,并对其进行处理。或者,如果它们只支持左到右重写,则使用标准C++默认参数。
- 型关于评论和投反对票:显然有一个新的政策,正如我得到的信息:"评论不能包含那个内容。不要评论你的否决。如果你认为这篇文章可以改进,请提供具体的指导。请参阅STACKOFFROUT.COM/Help /特权/评论。"(BTW,我的投票理由:一个关于如何在C++ 11/C++ 14/C++ 17中做事情的问题不应该用那些版本中没有的东西来回答——IMHO,注释是有建设性的,因为它指示了如何改进答案,但AI似乎不这么认为)。
- 型@我同意你的意见是建设性的。但我绝对不同意。因为gcc、clang和icc都在不同程度上支持指定的初始值设定项,而op并没有说明它们的编译器是什么。很有可能有人会遇到类似的问题,正好使用clang/gcc,并且能够使用它来解决他们的问题。这就是应该评判的答案:他们对和问题有同样问题的人有多大帮助?
好吧,这就是问题所在,在您的示例中,实际上根本不需要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)) {} |
这也意味着您可以为不同的成员提供其中的几个。因此,现在初始化成员的问题更为广泛。
事实上,你可以通过弗雷德·拉森在他对你的帖子的评论中提出的一些建议来消除对很多人的需求。
- 型太好了,如果有多个成员变量需要根据a1、a2、a3进行初始化,那么会怎么样?这个解决方案的规模比最初的答案更大,具体的好处是什么?
- 型@尼弗里德曼-那又怎样?upDBHandle2(open_database(a1, a2, a3))。和以前一样。
- 型太好了,所以每次添加或删除成员变量时,还必须在5中添加/删除它(您在默认构造函数中意外地留在init中)初始值设定项列表中。换句话说,您的答案有相当多的代码重复。这就是问题所在,即如何最好地避免。
- 型@尼弗里德曼-哦,你的答案如何初始化upDBHandle2?或者是upDBHandle2(open_database(p.a1, p.a2, p.a3)以某种方式减少了代码重复?
- 型我的答案只有一个初始值设定项列表,而不是5,因为我只有一个构造函数,而不是5,因为我以不同的方式处理默认值。所以是的,代码复制"不知何故"减少了。
- 型@如果你有多个成员都依赖于相同的参数,也许他们应该被分组到不同的类中。猜猜看!该类的构造函数与init()函数完全相同。所以在代码中看到这个模式对我来说就是"提取类重构"。
- 型@Sjoerd这是一个有效的答案,有其自身的缺点。同样,我不确定什么比init函数更具体,似乎很难相信它更容易阅读。但无论如何,我建议把它贴出来。但这不是这个答案;这个答案不能回答这个问题,因为这个问题说:我如何改进这个技术来消除重复?答案就是简单地重复,没有理由说重复比最初建议的技术更好。
- 型@尼弗里德曼-祝你好运,不要与没有默认状态的物体"复制"(尤其是与init一起)。哦,是的,我想类可以被扭曲成默认状态,但现在这是一种设计味道。
- @由于有一个默认的构造函数,标准库中的几乎每个类都有一个"默认状态"。问题需求没有任何问题,在许多语言(如python)中,您可以毫不犹豫地解决这个问题,而且人们一直在这样做。在C++ 20中,我的解决方案将符合C++,也可以完全解决问题,而不需要任何重复。你的回答不能回答问题,这并不意味着这是不可能的。
- @尼弗里德曼——我们中的一些人超越了标准库,不喜欢强迫一个不属于它的默认状态,只是为了安抚一些被误导的清洁感。我肯定回答的是这个问题,而不是你从中解释出来的问题。
- @讲故事的人问这个问题是为了避免重复。我们已经确定,添加到类中的每个成员都需要在5个不同的位置添加完全相同的代码行。如何避免重复?
- @尼弗里德曼-不,问题是这是否是2018年的好代码。更具体地说,自从首次发布C++ FAQ以来,助手EDOCX1的0个函数被认为是坏的。不管它们的可及性如何。
- @说书人好吧,如果你想直截了当的话:它仍然不能回答问题,因为你说他们的代码不好,而不展示你的解决方案更好,因为你甚至不承认它带来的重复,更不用说权衡你的"优势"(其中一个是代码味道,另一个相当奇怪)。
- 如果init函数返回唯一指针,然后如果以后需要更改,则只需要更改init,这样做会更好。
- @纳撒诺利弗,我同意,但同样的,这是不可扩展的两个成员。
- @纳撒诺利弗-公平点。我也可以把我最初的想法从评论中添加到操作中。
- @尼弗里德曼就是这样。为每个成员函数提供一个助手是很痛苦的。
- @讲故事的人说,init函数不好的主要原因是,它们鼓励两个阶段的初始化。private init函数可以是一种有用的技术;工厂函数返回可选的,调用private default constructor+init函数被称为在必须避免异常时处理失败的最佳方法。具有讽刺意味的是,在委托的构造函数可用之前,C++ FAQ特别推荐私有的init函数来共享代码:ISOCPP.Org/WiKi/Faq/Ctos。所以你说的完全是错误的。
- @尼弗里德曼-你是想链接一个特定的项目,还是我应该搜索一个你想到的项目?
- @说书人你可以C-F"分享"。老实说,这里的整个想法都很奇怪。有人有一些合理的要求,您坚持使用初始值设定项列表而不是init函数,尽管它们只在少数边缘情况下有优势,即使它们犯下了复制代码的更大的罪…也许现在是时候承认初始化器列表虽然对语言来说是自然的,而且是首选的,但是有一些主要的缺点,并且找到解决方法是合理的,即使这些解决方法也不理想?
- @尼弗里德曼-不,我认为合理的做法是停止沉迷于这场讨论。我不能证明你的论点。晚安。
- @说书人我想说5秒钟的努力超过了你是一个很好的方法来拯救面子,说了一些可以证明是错误的话。
- @尼弗里德曼-把这种廉价的心理学应用到你自己的学士学位上。我已经从我的电脑搬到我的手机上,躺在床上,看着它已经快2点了。所以不,我不会在电话里搜索。
- @说书人,什么时候连那个借口都不见了?isocpp.org/wiki/faq/ctors init方法
- @尼弗里德曼-我相信我有三分之二的子弹来掩盖这一点。"有时候"不是"你应该去争取。如果你自己的答案对2020年后来到这里的其他人是"有用的",那么我的答案对那些拥有const成员或非缺省类的人肯定是最有用的。现在,真的,晚安。
- "支持没有默认状态的对象。这些不能被默认初始化,请等待Init被调用。我想这是不可能的。在这种情况下,在默认构造函数中调用init,因此没有问题。
- @约翰内斯乔布·利特-也没有任何const对象。我试图提出一个更广泛的观点