C++核心准则ES.20: 保证所有对象被初始化

面向对象思考

共 7126字,需浏览 15分钟

 ·

2020-04-24 23:21

f48e7abdbda37e46d73f648129a5904f.webp

ES.20: Always initialize an object

ES.20: 保证所有对象被初始化


Reason(原因)

Avoid used-before-set errors and their associated undefined behavior. Avoid problems with comprehension of complex initialization. Simplify refactoring.

避免初始化之前使用以及相关联的未定义行为。避免复杂的初始化带来的理解问题。简化重构。


Example(示例)

void use(int arg)
{
int i; // bad: uninitialized variable
// ...
i = 7; // initialize i
}

No, i = 7 does not initialize i; it assigns to it. Also, i can be read in the ... part. Better:

不,i=7不是对i的初始化,而是给i赋值。同时,它可能在...省略的部分被使用。较好的做法:

void use(int arg)   // OK
{
int i = 7; // OK: initialized
string s; // OK: default initialized
// ...
}
Note(注意)

The always initialize rule is deliberately stronger than the an object must be set before used language rule. The latter, more relaxed rule, catches the technical bugs, but:

保证对象被初始化原则显然高于对象在使用前必须被设置原则。后面还有更宽松的原则可以捕捉错误,但是:

  • It leads to less readable code

  • 它会略微降低代码的可读性。

  • It encourages people to declare names in greater than necessary scopes

  • 它会鼓励人们将名称定义在超出必要的作用域中。

  • It leads to harder to read code

  • 它会带来更难读的代码。

  • It leads to logic bugs by encouraging complex code

  • 由于它会鼓励复杂代码从而引起逻辑错误。

  • It hampers refactoring

  • 难于重构

The always initialize rule is a style rule aimed to improve maintainability as well as a rule protecting against used-before-set errors.

确保对象初始化原则是一条致力于提高维护性的风格规则,也是可以防止设定之前使用的规则。


Example(示例)

Here is an example that is often considered to demonstrate the need for a more relaxed rule for initialization。

这段代码经常被用来证明有关初始化的更宽松规则的必要性。


widget i;    // "widget" a type that's expensive to initialize, possibly a large POD
widget j;

if (cond) { // bad: i and j are initialized "late"
i = f1();
j = f2();
}
else {
i = f3();
j = f4();
}

This cannot trivially be rewritten to initialize i and j with initializers. Note that for types with a default constructor, attempting to postpone initialization simply leads to a default initialization followed by an assignment. A popular reason for such examples is "efficiency", but a compiler that can detect whether we made a used-before-set error can also eliminate any redundant double initialization.

这段代码无法用一般的方法重写以便使用初始化器初始化i和j。注意对于包含默认构造函数的类型,延迟初始化的企图会简单的导致默认初始化后紧跟赋值。这个例子最常见的理由是为了效率,但是编译器可以检测出是否我们犯了设定前使用的错误,也能排除任何多余的初始化。


Assuming that there is a logical connection between i and j, that connection should probably be expressed in code:

假设i和j之间存在逻辑上的联系,这种联系可以通过下面的代码表达:

pair make_related_widgets(bool x)
{
return (x) ? {f1(), f2()} : {f3(), f4() };
}

auto [i, j] = make_related_widgets(cond); // C++17

If the make_related_widgets function is otherwise redundant, we can eliminate it by using a lambda ES.28:

如果不需要make_related_widgets函数,我们可以使用lambda表达式(ES.28)去掉它:

auto [i, j] = [x]{ return (x) ? pair{f1(), f2()} : pair{f3(), f4()} }();    // C++17

Using a value representing "uninitialized" is a symptom of a problem and not a solution:

使用数值来表现“未初始化”是一种问题征兆,而不是解决方案。


widget i = uninit;  // bad
widget j = uninit;

// ...
use(i); // possibly used before set
// ...

if (cond) { // bad: i and j are initialized "late"
i = f1();
j = f2();
}
else {
i = f3();
j = f4();
}

Now the compiler cannot even simply detect a used-before-set. Further, we've introduced complexity in the state space for widget: which operations are valid on an uninit widget and which are not?

这种状态下,编译器无法简单的检测到设定前使用问题。另外,我们在widget的状态空间中引入了复杂性。对于uninit的widget来讲,哪个操作是合法的,哪个是非法的?


Note(注意)

Complex initialization has been popular with clever programmers for decades. It has also been a major source of errors and complexity. Many such errors are introduced during maintenance years after the initial implementation.

对于聪明的程序员来讲,复杂初始化早已经不是什么新鲜事了。它同时也是错误和复杂性的主要来源之一。很多这样的错误都是在首次实现很多年之后的维护中引入的。


Example(示例)

This rule covers member variables.

这条规则也适用于成员变量。

class X {
public:
X(int i, int ci) : m2{i}, cm2{ci} {}
// ...

private:
int m1 = 7;
int m2;
int m3;

const int cm1 = 7;
const int cm2;
const int cm3;
};

The compiler will flag the uninitialized cm3 because it is a const, but it will not catch the lack of initialization of m3. Usually, a rare spurious member initialization is worth the absence of errors from lack of initialization and often an optimizer can eliminate a redundant initialization (e.g., an initialization that occurs immediately before an assignment).

由于cm3是一个常数,编译器会提示它没有被初始化,但是m3没有被初始化这件事会被漏掉。通常,一个很少用到的伪成员初始化可以避免初始化遗漏并且通常优化器可以去掉多余的初始化。(例如,紧接在赋值之前的初始化)


Exception(例外)

If you are declaring an object that is just about to be initialized from input, initializing it would cause a double initialization. However, beware that this may leave uninitialized data beyond the input -- and that has been a fertile source of errors and security breaches:

如果你声明一个只希望根据输入信息初始化的对象,对它进行初始化将会导致双重初始化。然而,需要小心超出输入范围的未初始化数据--这已经成为很多错误和安全问题的来源。

constexpr int max = 8 * 1024;
int buf[max]; // OK, but suspicious: uninitialized
f.read(buf, max);

The cost of initializing that array could be significant in some situations. However, such examples do tend to leave uninitialized variables accessible, so they should be treated with suspicion.

某些情况下初始化数组的代价可能会很巨大。然而,这样的例子往往会产生没有初始化的可访问数据,它们应该被视作不安全的。

constexpr int max = 8 * 1024;
int buf[max] = {}; // zero all elements; better in some situations
f.read(buf, max);

Because of the restrictive initialization rules for arrays and std::array, they offer the most compelling examples of the need for this exception.

由于数组和std::array的限制性初始化规则,它们为本例外提供了更加有说服力的示例。

When feasible use a library function that is known not to overflow. For example:

如果可能,使用已知不会溢出的库函数,例如:

string s;   // s is default initialized to ""
cin >> s; // s expands to hold the string

Don't consider simple variables that are targets for input operations exceptions to this rule:

不要认为为输入操作准备的简单的变量是本规则的例外。

int i;   // bad
// ...
cin >> i;

In the not uncommon case where the input target and the input operation get separated (as they should not) the possibility of used-before-set opens up.

如果输入对象和输入操作分离(本不应该这样),设定前使用的可能性就会增加。这是很常见的情况。

int i2 = 0;   // better, assuming that zero is an acceptable value for i2
// ...
cin >> i2;

A good optimizer should know about input operations and eliminate the redundant operation.

好的优化器应该了解输入操作并且消除多余的操作。


Note(注意)

Sometimes, a lambda can be used as an initializer to avoid an uninitialized variable:

有时lambda表达式可以用作避免未初始化变量的初始化器。

error_code ec;
Value v = [&] {
auto p = get_value(); // get_value() returns a pair
ec = p.first;
return p.second;
}();

or maybe(也可以这样):

Value v = [] {
auto p = get_value(); // get_value() returns a pair
if (p.first) throw Bad_value{p.first};
return p.second;
}();

See also: ES.28(参见ES.28)


Enforcement(实施建议)

  • Flag every uninitialized variable. Don't flag variables of user-defined types with default constructors.

  • 提示所有未初始化变量。具有默认构造函数的用户定义类型应该除外。

  • Check that an uninitialized buffer is written into immediately after declaration. Passing an uninitialized variable as a reference to non-const argument can be assumed to be a write into the variable.

  • 检查没有初始化的缓冲区被声明之后马上被写入的情况。以非常量引用参数的方式传递一个未初始化变量可以认为是对该变量的写入。


原文链接

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#es20-always-initialize-an-object




觉得本文有帮助?请分享给更多人。

关注微信公众号【面向对象思考】轻松学习每一天!

面向对象开发,面向对象思考!


浏览 51
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报