Document Number: N2354=07-0214
Date: 2007-07-19
Reply to: Bill Seymour <bill-at-the-office@pobox.com>

Class member initializers

Michael Spertus
Bill Seymour

Abstract

We propose allowing the use of initializers for non-static class and struct data members. The purpose of this is to increase maintainability, reduce the risk of subtle errors in complex program code, and to make the use of initializers more consistent.


The proposal

The basic idea is to allow non-static data members of class and struct types to be initialized where declared. All of the same initialization syntaxes may be used as for initialization of local variables; and other analyses and suggestions for improving initialization of local variables as in N1493, N1509, N1584, N1701, N1806, N1824, and N1919 may also be applied mutatis mutandis to non-static class and struct data members.

As a simple example,

    class A {
    public:
        int a = 7;
    };
would be equivalent to
    class A {
    public:
        A() : a(7) {}
    };
The real benefits of member initializers do not become apparent until a class has multiple constructors. For many data members, especially private ones, all constructors initialize a data member to a common value as in the next example:
    class A {
    public:
        A(): a(7), b(5), hash_algorithm("MD5"), s("class A example") {}
        A(int a_val) : a(a_val), b(5), hash_algorithm("MD5"), s("Constructor run") {}
        A(int b_val) : a(7), b(b_val), hash_algorithm("MD5"), s("Constructor run") {}
        A(D d) : a(f(d)), b(g(d)), hash_algorithm("MD5"), s("Constructor run") {}
        int a, b;
    private:
        // Cryptographic hash to be applied to all A instances
        HashingFunction hash_algorithm;
        // String indicating state in object lifecycle
        std::string s;
    };
Even in this simple example, the redundant code is already problematic if the constructor arguments for hash_algorithm are copied incorrectly in one of A’s constructors or if one of the lifecycle states was accidentally misspelled as "Constructor Run". These kinds of errors can easily result in subtle bugs. Such inconsistencies are readily avoided using member initializers.
    class A {
    public:
        A(): a(7), b(5) {}
        A(int a_val) : a(a_val), b(5) {}
        A(int b_val) : a(7), b(b_val) {}
        A(D d) : a(f(d)), b(g(d)) {}
        int a, b;
    private:
        // Cryptographic hash to be applied to all A instances
        HashingFunction hash_algorithm("MD5");
        // String indicating state in object lifecycle
        std::string s("Constructor run");
    };
Not only does this eliminate redundant code that must be manually synched, it makes much clearer the distinctions between the different constructors. (Indeed, in Java, where both forms of initialization are available, the use of member initializers is invariably preferred by experienced Java programmers in examples such as these.)

Now suppose that it is decided that MD5 hashes are not collision resistent enough and that SHA-1 hashes should be used. Without member initializers, all the constructors need to be updated. Unfortunately, if one developer is unaware of this change and creates a constructor that is defined in a different source file and continues to initialize the cryptographic algorithm to MD5, a very hard to detect bug will have been introduced. It seems better to keep the information in one place.

It may happen that a data member will usually have a particular value, but a few specialized constructors will need to be cognizant of that value. If a constructor initializes a particular member explicitly, the constructor initialization overrides the member initializations as shown below:

    class A {
    public:
        A(): a(7), b(5) {}
        A(int a_val) : a(a_val), b(5) {}
        A(int b_val) : a(7), b(b_val) {}
        A(D d) : a(f(d)), b(g(d)) {}
        // Copy constructor
        A(const A& aa) : a(aa.a),
                         b(aa.b),
                         hash_algorithm(aa.hash_algorithm.getName()),
                         s(aa.s) {}
        int a, b;
    private:
        // Cryptographic hash to be applied to all A instances
        HashingFunction hash_algorithm("MD5");
        // String indicating state in object lifecycle
        std::string s("Constructor run");
    };
A few additional points are worth noting.


Suggested changes to the Working Paper, N2315

7.1.5.4 auto specifier

In paragraph [4], change “a constant-initializer” to “an initializer”.


9.2 Class Members

Change the second line of member-declarator which reads
declarator  constant-initializeropt
to
declarator  initializeropt


Delete the rule
constant-initializer:
    = constant-expression


Delete paragraph 9.2[4] which reads
A member-declarator can contain a constant-initializer only if it declares a static member (9.4) of const integral or const enumeration type, see 9.4.2.


9.4.2 Static data members

In the first sentence of [3] which currently reads
If a static data member is of const integral or const enumeration type, its declaration in the class definition may specify a constant-initializer whose constant-expression shall be an integral constant expression (5.19).
change
a constant-initializer whose constant-expression shall be
to
an initializer whose initializer-clause or expression-list shall be


12.6.2 Initializing bases and members

Append to the third bullet of 12.6.2[5] which begins “Then, non-static data members”
If a constructor has a mem-initializer for a non-static data member that has an initializer, the initialization specified by the mem-initializer shall be performed, and the non-static data member’s initializer shall be ignored.

[ Example: given

struct A {
    int i = /* some integer expression with side effects */ ;
    A(int arg) : i(arg) { }
    // ...
};
the A(int) constructor will simply initialize i to the value of arg, and the side effects in i’s initializer will not take place. — end example ]


12.8 Copying class objects

Add before the last sentence of [8] which begins “Virtual base class subobjects”
An implicitly declared copy constructor shall ignore any non-static data member’s initializer. [ Note: this implies that any side effect in such an initializer will not take place. See also the example in 12.6.2[5]. — end note ]