只要你定义了一个带有 constructor(构造函数)或 destructor(析构函数)的类型的变量,当控制流程到达变量定义的时候会使你担负构造成本,而当变量离开作用域的时候会使你担负析构成本。如果这一成本关联到无用变量,你就要尽你所能去避免它。
你或许认为你从来不会定义无用变量,但是也许你应该再想一想。考虑下面这个函数,只要一个 password 足够长,它就返回这个 password 的加密版本。如果 password 太短,函数就会抛出一个定义在标准 C++ 库中的
logic_error
类型的异常:
// this function defines the variable "encrypted" too soon
std::string encryptPassword(const std::string& password)
{
using namespace std;
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
... // do whatever is necessary to place an
// encrypted version of password in encrypted
return encrypted;
}
throw logic_error("Password is too short");
}
... // do whatever is necessary to place an
// encrypted version of password in encrypted
return encrypted;
}
object
encrypted
在这个函数中并不是
completely
(完全)无用,但是如果抛出了一个异常,它就是无用的。也就是说,即使
encryptPassword
抛出一个异常,你也要为构造和析构
encrypted
付出代价。因此:你最好将
encrypted
的定义推迟到你确信你真的需要它的时候:
// this function postpones encrypted's definition until it's truly necessary
std::string encryptPassword(const std::string& password)
{
using namespace std;
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
throw logic_error("Password is too short");
}
string encrypted;
... // do whatever is necessary to place an
// encrypted version of password in encrypted
return encrypted;
}
// encrypted version of password in encrypted
return encrypted;
}
这一代码仍然没有达到它本可以达到的那样紧凑,因为定义
encrypted
的时候没有任何 initialization arguments(初始化实参)。这就意味着将使用它的 default constructor(缺省构造函数),在很多情况下,对于一个 object 你首先应该做的就是给它一些值,这经常可以通过 assignment(赋值)来完成。default-constructing(缺省构造)一个 object 然后赋值给它比用你真正需要它持有的值初始化它更低效。
例如,假设
encryptPassword
的核心部分是在这个函数中完成的:
void encrypt(std::string& s); // encrypts s in place
那么,
encryptPassword
就可以这样实现,虽然它还不是最好的方法:
// this function postpones encrypted's definition until
// it's necessary, but it's still needlessly inefficient
std::string encryptPassword(const std::string& password)
{
... // check length as above
// it's necessary, but it's still needlessly inefficient
std::string encryptPassword(const std::string& password)
{
... // check length as above
string encrypted;
// default-construct encrypted
encrypted = password; // assign to encrypted
encrypted = password; // assign to encrypted
encrypt(encrypted);
return encrypted;
}
return encrypted;
}
一个更可取得方法是用
password
初始化
encrypted
,从而跳过毫无意义并可能很昂贵的 default construction(缺省构造):
// finally, the best way to define and initialize encrypted
std::string encryptPassword(const std::string& password)
{
... // check length
std::string encryptPassword(const std::string& password)
{
... // check length
string encrypted(password);
// define and initialize
// via copy constructor
// via copy constructor
encrypt(encrypted);
return encrypted;
}
return encrypted;
}
这个建议就是本标题中的 "as long as possible"(“只要有可能”)的真正含义。你不仅应该推迟一个变量的定义直到你不得不用它之前的最后一刻,而且应该尝试推迟它的定义直到你得到了它的 initialization arguments(初始化实参)。通过这样的做法,你可以避免构造和析构不必要的 objects,而且还可以避免不必要的 default constructions(缺省构造)。更进一步,通过在它们的含义已经非常明确的上下文中初始化它们,有助于对变量的作用文档化。
“但是对于循环会如何?”你可能会有这样的疑问。如果一个变量仅仅在一个循环内使用,是循环外面定义它并在每次循环迭代时赋值给它更好一些,还是在循环内部定义这个变量更好一些呢?也就是说,下面这两个大致的结构中哪个更好一些?
//
Approach A:
define outside loop //
Approach B:
define inside loop
Widget w;
for (int i = 0; i < n; ++i){ for (int i = 0; i < n; ++i) {
w = some value dependent on i ; Widget w( some value dependent on i ) ;
... ...
} }
for (int i = 0; i < n; ++i){ for (int i = 0; i < n; ++i) {
w = some value dependent on i ; Widget w( some value dependent on i ) ;
... ...
} }
这里将一个
string
类型的 object 换成了一个
Widget
类型的 object,以避免对这个 object 的构造、析构或赋值操作的成本的任何先入为主的预见。
对于
Widget
的操作而言,就是下面这两个方法的成本:
方法 A:
1
个 constructor(构造函数) +
1
个 destructor(析构函数) +
n
个 assignments(赋值)。
方法 B:
n
个 constructors(构造函数) +
n
个 destructors(析构函数)。
对于那些一个 assignment(赋值)的成本低于一个 constructor-destructor pair(构造函数/析构函数对)的成本的 classes,方法 A 通常更高效。特别是在
n
变得很大的情况下。否则,方法 B 可能更好一些。此外,方法 A 与方法 B 相比,使得名字
w
在一个较大的范围(包含循环的那个范围)内均可见,这可能会破坏程序的易理解性和可维护性。因此:除非你确信以下两点:(1)assignment(赋值)比一个 constructor-destructor pair(构造函数/析构函数对)成本更低,而且(2)你正在处理你的代码中的性能敏感的部分,否则,你应该默认使用方法 B。
Things to Remember
只要有可能就推迟 variable definitions(变量定义)。这样可以增加程序的清晰度并提高程序的性能。