C++ ctors:在.cpp文件中使用初始化列表有什么意义?

Mor*_*hai 1 c++ performance

C++有一个支持ctor的初始化列表的这个时髦的怪癖,例如:

class Foo
{
public:
  Foo(int x) : m_x(x) { }
private:
  SomeComplexObjectThatTakesAnIntForConstruction m_x;
}
Run Code Online (Sandbox Code Playgroud)

到目前为止有道理.更高效,因为成员只初始化一次,而不是默认构造,然后operator =稍后赋值.

但是我经常遇到那些将ctor放在他们的.cpp文件中的程序员,我几乎无法相信它实际上具有正确使用初始化列表的预期(有效)效果:

// Foo.cpp
Foo::Foo(int x) : m_x(x)
{
  // complex set of things needed to be done, or perhaps dependency-inducing references here...
}
Run Code Online (Sandbox Code Playgroud)

据我所知,上面不一定会为m_x生成一个构造,因为初始化列表在这个翻译单元之外是不可见的,并且会导致构造+赋值,不是吗?

// user.cpp
Foo my_foo(9);  // how can the ctor for m_x be effectively inlined here?
Run Code Online (Sandbox Code Playgroud)

或者我误解了初始化列表的功能如何?

感谢您对此的帮助;)


我选择将初始化列表和构造体分成两部分,例如:

class Foo
{
public:
  Foo(int x) : m_x(x) { Initialize(); }
private:
  void Initialize(); // defined in our .cpp thus isolating dependencies and creating a common call-point for multiple ctors (if present)
  SomeComplexObjectThatTakesAnIntForConstruction m_x;
}
Run Code Online (Sandbox Code Playgroud)

Nic*_*yer 15

你误解了.

初始化程序列表不需要从其他翻译单元可见,就像构造函数体不需要从其他翻译单元可见一样.它会影响构造函数本身生成的代码,而不是为调用构造函数而生成的代码.


也许这会消除困惑:

内联是一种特殊的优化.它不是唯一可能的优化类型.现代C++编译器能够执行各种其他优化(循环展开,当它们不影响程序行为时重新排序语句等).

内联为您提供的"捷径"或"效率增益"消除了在调用堆栈上创建新帧的需要.通常,为函数调用生成的代码看起来像这样,其中前缀的行是--被调用函数的一部分(假设C调用约定).

Push the arguments on to the stack
Push the current code address onto the stack
Jump to the address of the function
-- Move the stack pointer forward to create space for local variables
-- Execute the body of the function
-- Move the stack pointer back to remove the local variables
-- Pop the caller's address from the stack and jump to it
Pop the arguments from the stack
Run Code Online (Sandbox Code Playgroud)

如果函数是内联的,那么这只是被调用函数执行的前三个步骤:

-- Move the stack pointer forward to create space for local variables
-- Execute the body of the function
-- Move the stack pointer back to remove the local variables
Run Code Online (Sandbox Code Playgroud)

这种优化依赖于编译器和/或连接的变化的能力在那里生成的代码,没有什么是生成的代码.

相比之下,初始化列表会影响什么是生成的代码,而不是在那里它被生成.无论是直接在调用站点还是在调用将跳转到的程序代码的单独部分中,编译器仍然可以为成员变量生成对非默认构造函数的调用.

  • @Mordachai:@Nick Meyer:不对.@Noah Roberts是对的.即使源在使用点不可见,编译器也能够内联.它是内部编译单元优化的一部分(现在确实这是非常重要的,你的基本编译器也不会烦恼)但是好的编译器(比如gcc/cc1)完全有能力做到这一点.例如.`g ++ ac bc`这里ac的源在bc中不可见(反之亦然)但是编译器有足够的信息来在两个编译单元中执行内联代码.您可以看到dev-studio试图利用这一点(查看日志). (3认同)
  • 由于这句话:"...这种类型的优化确实需要编译器在调用它时知道函数体." 它不仅是错误的,它正是让人们通过所有这些环节来"优化"代码的东西.可以在*link*时发生各种各样的优化,包括内联.好的编译器套件非常擅长它. (2认同)

Mic*_*urr 8

在.cpp文件中实现时,初始化程序列表工作正常 - 是什么让你相信它们不会?

初始化列表仍然是构造函数"call"的一部分.它只是一种语法,它规范了如何构建类成员(注意新手 - 它不指导或影响类成员构造的顺序,但它允许将参数传递给成员的构造函数).这使得简单的规则成为可能,即当到达开始括号之后的第一个语句时,所有类成员都已经完成了它们的构造,但这并不意味着初始化程序列表需要在构造函数被调用之前发生.


解决Mordachai的评论:

在头文件与.cpp文件中使用init列表会影响构造函数(或初始化列表)的"内联能力",如果你将构造函数的主要工作推迟到内联ctor中的函数调用).但是,对于成员函数的任何in-header实现与.cpp文件中的实现相比,情况都是如此.

我怀疑对于大多数ctors来说,性能问题将归因于资源分配 - 如果他们没有获得资源,他们可能不会出现性能问题 - 并且无论ctor是什么,这都需要相同的时间.内联与否.请注意,这仍然意味着init列表很重要,无论它们是否内联,因为它们可以防止(在您的问题中提到的)情况:

  • 默认初始化成员对象(可能在昂贵的操作中获取资源)
  • 重新初始化成员对象(可能导致释放资源,然后获取新资源)

由于与许多其他事物相比,资源获取/释放通常很昂贵(无论该资源是内存,网络连接,打开文件),这是一个需要避免的重要反模式.但是,我认为,在大多数情况下,这些资源获取是否内联之间的性能差异可能并不显着.

当然,初始化列表也解决了正确性问题.例如,由于const无法修改成员,因此必须在初始化列表中初始化它们.

  • @JimR:我希望所有的C++编译器都会发出警告(实际上,我希望这是一个错误).他们警告的原因正好是*因为*init列表不确定init-order,但它读起来就像它一样.即使是有经验的程序员(至少是我)也可能会被欺骗,因为你通常没有记住成员声明顺序,阅读初始列表会让你相信某些可能不正确的事情.大多数时候它并不重要,但有时成员对象初始化之间存在依赖关系. (3认同)

Cas*_*Cow 5

初始化列表的目的不仅仅是效率问题.

除了必须在那里初始化成员的情况,因为没有其他方法(引用,const成员,没有默认构造函数的类成员),它通常是"首选",就像你第一次初始化变量一样宣布他们.

在某些情况下,最好使用构造函数体将变量设置为正确的值,例如,如果有两个指针指向使用new创建的对象,并且您可能会害怕第二个可能抛出的对象.在这种情况下,您仍然应该"初始化"它们 - 为NULL - 然后在正文中创建它们,第一个在auto_ptr中以防万一(在第二个工作之后释放).

将构造函数体移动到编译单元的目的是从接口隐藏实现细节.这通常优选用于可维护性,其中大部分时间比节省微秒的少量运行时效率更重要.

  • 您也应该为成员使用智能指针,然后在体内初始化它们的必要性消失了.稍后初始化引发的异常将导致先前的破坏. (2认同)