我目前正在阅读Bjarne Stroustrup的"C++编程语言:特别版",在第133页上,它陈述了以下内容:
对于用户定义的类型,将变量的定义推迟到一个合适的初始化器也可以带来更好的表演例如:
string s; /* .... */ s = "The best is the enemy of the good.";
很容易比慢得多
string s = "Voltaire";
我知道它表示可以很容易地,这意味着它不一定是这样,但我们只能说它确实发生了。
为什么这会使潜在性能提高?
是只有用户定义类型(甚至STL类型)才如此,还是int
、float
等也是如此?
我认为这主要是关于具有非平凡默认构造函数的类型,至少就性能而言是这样。
这两种方法的区别在于:
- 在第一个版本中,首先构造一个空字符串(使用默认构造函数);然后使用赋值运算符来有效地丢弃默认构造函数所做的工作,并将新值分配给字符串
- 在第二个版本中,在构造点立即设置所需的值
当然,很难事先判断这会带来多大的性能差异。
-
执行默认构造函数 需要时间。在随后调用的赋值运算符中重写它初始化字符串的内容也需要时间。
-
当函数(由于
return
语句或异常)位于默认构造函数和赋值运算符的调用之间时,执行可能永远不会到达赋值。在这种情况下,对象被默认初始化为不必要的。 -
如果抛出异常,实现可能会浪费性能以确保对象的析构函数被调用。如果对象在从未到达的后续作用域中初始化,则也不需要。
因为:
string s; /* .... */ s = "The best is the enemy of the good.";
涉及两个操作:施工和分配
While:
string s = "Voltaire";
仅涉及施工。
这相当于在构造函数主体中选择成员初始化器列表而不是赋值。
这是个好问题。你是对的,这种情况只发生在复杂的类型中。即类和结构,std::string就是这样一个对象。这里涉及的真正问题与构造函数有关。
创建对象时,即
std::string s;
它的构造函数被调用,它可能会分配一些内存,进行一些其他变量初始化,使自己可以使用。事实上,在代码的这一点上可以执行大量的代码。
稍后你做:
s = "hello world!";
这导致类不得不放弃它所做的大部分工作,并准备用新字符串替换它的内容。
如果您在定义变量时设置值,即:,则这实际上会减少为一次操作
std::string s = "Hello world";
事实上,如果您在调试器中观察代码,请执行一次不同的构造函数,而不是构造对象,然后单独设置值。事实上,前面的代码与相同
std::string s("Hello world");
我希望这有助于澄清问题。
考虑一下这两种情况下会发生什么。在第一种情况下:
- 为"s"调用了默认构造函数
- 为"s"调用了赋值运算符
在第二种情况下,首先考虑复制省略,这相当于string s("Voltaire")
,因此:
- 调用了c字符串构造函数
从逻辑上讲,第一种方法需要抽象机器做更多的工作。这是否真的转化为更真实的代码取决于实际的类型以及优化器能做多少。尽管要注意,对于除了琐碎的用户类型之外的所有用户类型,优化器可能不得不假设默认构造函数有副作用,因此不能简单地删除它。
此额外成本应仅适用于用户类型,因为成本在默认构造函数中。对于任何像int这样的基元类型,或者实际上是任何具有琐碎构造函数/副本的基元,默认构造函数都没有成本——数据根本不会被初始化(在函数范围内时)。
为什么这会提高潜在的性能?
第一种情况涉及默认初始化,然后是赋值;第二个涉及从值初始化。默认初始化可能会做一些工作,这些工作后来必须通过赋值来重做(甚至撤消),因此第一种情况可能比第二种情况涉及更多的工作。
是只有用户定义类型(甚至STL类型)才如此,还是int、float等也是如此?
只有用户定义的类型才是如此;然后它取决于构造函数和赋值运算符的实际操作。对于标量类型,默认初始化什么都不做,而赋值与从值初始化做的事情相同,所以两种选择都是等效的。
该类有三种初始化字符串的方法:
string s; // Default constructor
string s = "..."; // Default constructor followed by operator assignment
string s("..."); // Constructor with parameters passed in
字符串类需要分配内存。最好在它知道需要多少内存后再进行分配。