C++核心準則ES.20: 保證所有對象被初始化

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<widget, widget> 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<error_code, Value>
    ec = p.first;
    return p.second;
}();

or maybe(也可以這樣):

Value v = [] {
    auto p = get_value();   // get_value() returns a pair<error_code, Value>
    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

 


 

覺得本文有幫助?歡迎點贊並分享給更多的人。

閱讀更多更新文章,請關注微信公衆號【面向對象思考】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章