Document Number: N2426=07-0296
Date: 2007-10-02
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, N2369

5.1 Primary expressions [expr.prim]

Add “or in an initializer for a non-static data member (9.2)” to 5.1p3:
The keyword this names a pointer to the object for which a non-static member function (9.3.2) is invoked. The keyword this shall be used only inside a non-static class member function body (9.3) or in an initializer for a non-static data member (9.2). The type of the expression is a pointer to the function’s class (9.3.2), possibly with cv-qualifiers on the class type. The expression is an rvalue.


Add a bullet to 5.1p11:
An id-expression that denotes a non-static data member or non-static member function of a class can only be used:
— as part of a class member access (5.2.5) in which the object-expression refers to the member’s class or a class derived from that class, or

— to form a pointer to member (5.3.1), or

— in the body of a non-static member function of that class or of a class derived from that class (9.3.1), or

— in a mem-initializer for a constructor for that class or for a class derived from that class (12.6.2), or

— in an initializer for a data member of that class or of a class derived from that class (12.6.2), or

— if that id-expression denotes a non-static data member and it is the sole constituent of an unevaluated operand, except for optional enclosing parentheses. [ Example:
...


7.1.6.4 auto specifier [dcl.spec.auto]

Change paragraph 4:
The auto type-specifier can also be used in declaring an object in the condition of a selection statement (6.4) or an iteration statement (6.5), in the type-specifier-seq in a new-type-id (5.3.4), and in declaring a static data member with a constant-initializer an initializer that appears within the member-specification of a class definition (9.4.2).


9.2 Class members [class.mem]

Change constant-initializer to initializer in the second line of member-declarator:
member-declarator:
    declarator  pure-specifieropt
    declarator  constant-initializer initializeropt
    identifieropt  :  constant-expression


Delete the final rule entirely:
constant-initializer:
    = constant-expression


Delete 9.2p4 entirely:
A member-declarator can contain a constant-initializer only if it declares a static member (9.4) of const literal type, see 9.4.2.


9.2.4 Static data members [class.static.data]

In two places in 9.2.4p3, change “ aconstant-initializer” to “an initializer that is a constant expression (5.19)”:
If a static data member is of const literal type, its declaration in the class definition can specify a constant-initializer an initializer that is a constant expression (5.19). A static data member of literal type can be declared in the class definition with the constexpr specifier; if so, its declaration shall specify a constant-initializer an initializer that is a constant expression (5.19). In both these cases, the member may appear in integral constant expressions. The member shall still be defined in a namespace scope if it is used in the program and the namespace scope definition shall not contain an initializer.


12.1 Constructors [class.ctor]

Add a new bullet between the first and second in 12.1p5:
A default constructor for a class X is a constructor of class X that can be called without an argument. If there is no user-declared constructor for class X, a default constructor is implicitly declared. An implicitly-declared default constructor is an inline public member of its class. A default constructor is trivial if it is not user-provided (8.4) and if:

— its class has no virtual functions (10.3) and no virtual base classes (10.1), and

— no non-static data member of its class has an initializer, and

— all the direct base classes of its class have trivial default constructors, and

— for all the non-static data members of its class that are of class type (or array thereof), each such class has a trivial default constructor.


12.6.2 Initializing bases and members [class.base.init]

Add a new paragraph and an example between 12.6.2p3 and 12.6.2p4:
If a given non-static data memter has both an initializer and a mem-initializer, the initialization specified by the mem-initializer is performed, and the non-static data member’s initializer is 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 ]


Add to the first sentence of the current 12.6.2p4:

If a given non-static data member or base class is not named by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer), and if the given non-static data member has no initializer, then


12.7 Construction and destruction [class.cdtor]

Add “initializer or” in the second sentence of 12.7p4:
Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2). When a virtual function is called directly or indirectly from a constructor (including from the initializer or mem-initializer for a data member) or from a destructor, and the object to which the call applies is the object under construction or destruction, the function called is the one defined in the constructor or destructor’s own class or in one of its bases, but not a function overriding it in a class derived from the constructor or destructor’s class, or overriding it in one of the other base classes of the most derived object (1.8). If the virtual function call uses an explicit class member access (5.2.5) and the object-expression refers to the object under construction or destruction but its type is neither the constructor or destructor’s own class or one of its bases, the result of the call is undefined. [ Example:
...


Same for 12.7p5:

The typeid operator (5.2.8) can be used during construction or destruction (12.6.2). When typeid is used in a constructor (including from the initializer or mem-initializer for a data member) or in a destructor, or used in a function called (directly or indirectly) from a constructor or destructor, if the operand of typeid refers to the object under construction or destruction, typeid yields the std::type_info object representing the constructor or destructor’s class. If the operand of typeid refers to the object under construction or destruction and the static type of the operand is neither the constructor or destructor’s class nor one of its bases, the result of typeid is undefined.


And for 12.7p6:

Dynamic_casts (5.2.7) can be used during construction or destruction (12.6.2). When a dynamic_cast is used in a constructor (including from the initializer or mem-initializer for a data member) or in a destructor, or used in a function called (directly or indirectly) from a constructor or destructor, if the operand of the dynamic_cast refers to the object under construction or destruction, this object is considered to be a most derived object that has the type of the constructor or destructor’s class. If the operand of the dynamic_cast refers to the object under construction or destruction and the static type of the operand is not a pointer to or object of the constructor or destructor’s own class or one of its bases, the dynamic_cast results in undefined behavior.


12.8 Copying class objects [class.copy]

Add a note after the first sentence of 12.8p8:
The implicitly-defined or explicitly-defaulted copy constructor for class X performs a memberwise copy of its subobjects. [ Note: initializers of non-static data members are ignored. See also the example in 12.6.2p4. — end note ] The order of copying is the same as the order of initialization of bases and members in a user-defined constructor (see 12.6.2). Each subobject is copied in the manner appropriate to its type: