Tuesday, 3 June 2014

C++11 - Initializer-list (Part I)

Initializer-list curly brackets "{}" is used to initialize variables/objects and it does not replace creation/copy/move constructors. C++03 inherits the C-style initializer-list features and it works only on build-in types and aggregate. C+11 extends this features to all other standard class and STL containers. And it adds another type of constructor, initializer-list constructor.

1. Limitations on C++03 initializer-list
C++03 inherits the C's initializer-list features and keeps the promise of maintaining the back compatibility toward old C++ standard and C standard. Therefore the very basic requirements in C standard are still kept valid in C++ standard. For initializer-list particular features like static initialization and known memory footprint at compiling time are well maintained in C++03.

Works only on build-in types and aggregate
More details about aggregate and initializer-list please refer to my other blog entry, Aggregate on C++11. And more about plain old data (POD) and initializer-list, please refer to Plain Old Data (POD) on C++11.

Example 1: initializer-list on C++03
//********************************************************************************
int i = {0};

struct Foo{
    int x;
    double y;
};

Foo = {1, 1.0};

Foo arr[3] = {Foo()};
//********************************************************************************

There is is long list of requirements for classes that can be qualified as aggregate or POD. Refer to those requirements on Aggregate on C++11 and Plain Old Data (POD). Most of standard classes are no near to these requirements. Therefore in C++03 initialzier-list has very limited use.

Does not work with STL containers
This could be one of most noticeable drawbacks in C++03 initializer-list, because it makes perfect sense that initializer-list should work on STL container. For instance std::vector internally it could just be an array implementation. However in C++03 initializer-list works on all arrays but does not on std::vector. Really no good excuse to justify this.

Example 2: initialier list and std::vector
//********************************************************************************
// C++03
std::vector<int> myVec = {3, 7, 6, 5,11}; // Error
std::vector<int> myVec{3, 7, 6, 5, 11}; // Error
std::vector<int> myVec(3); // Ok, but not what we want. {0, 0, 0}
std::vector<int> myVec(3, 3); // Ok, but not what we want {3, 3, 3}

// C++03: to initialize to {3, 7, 6, 5, 11}
// Solution 1:
std::vector<int> myVec;
myVec.reserve(5);
myVec.push_back(3);
myVec.push_back(7);
myVec.push_back(6);
myVec.push_back(5);
myVec.push_back(11);

// C++03: better way to initialize to {3, 7, 6, 5, 11}
// Solution 2:
int a[] = {3, 7, 6, 5, 11}
std::vector<int> myVec(a, a+5);
//********************************************************************************

Solution 1 in Example 2 shows how tedious it would be in order to initialize 5 values. And Solution 2 is not a bad one. But when it comes to multi-dimensional vector, std::vector<std::vector<int> >, it would not have a good solution. It would have to repeatedly use the technique, shown in Solution 2 in Exmaple 1, to initialize the one dimension std::vector and repeatedly push back to a 2 dimensional vector, and so on, to get multi-dimensional vector. This is definitely worth of improving.

Most vexing parse problem
This is one specific syntactic ambiguity problem when using "()" to initialize variables/objects. This is because of multiple usage of brackets "()". It can be used for object initialization, function calling and operator.

// Example 3: "()"
//********************************************************************************
struct Foo {
    Foo();
    Foo(int x);
    int operator()(int x);
};
Foo* CreateFoo(int);

int x = 2;
Foo foo(x); // objection creation
CreateFoo(x); // function calling
foo(x); // operator()

struct Bar {
    Bar(Foo);
};

// most of expert C++ programmer would say this is to create an object "my_bar"
// but surprisingly the compiler will say option 2.
Bar my_bar(Foo()); // ambiguity
                               // 1. create a Bar object "my_bar"
                               // 2. function declaration:
                               //     function name - my_bar
                               //     function return type - "Bar" ( a Bar object)
                               //     function argument: a function pointer - return Foo and take no argument

// C++03 workaround
Bar my_bar((Foo()); // Ok - force the compiler to evaluate (Foo()) first
//********************************************************************************

Please read more about this topic on Most vexing parse. This is one of motivations to use curly brackets "{}" to initialize objects rather than using "()".

2. C+11 initializer-list fixes all above 3 problems and provides more
The functionality of initializer-list in C++11 has been hugely expanded. It can work on all classes, work on all STL containers and provide a initializer-list constructor. But still keep the promise of maintaining the back compatibility towards older C++ standard and C standard.

Compatibility maintained on aggregate and POD
C++11 has some improvements on these two features due to expansion of other features but still all the promise on aggregate and POD are still maintained in C++11. Read Aggregate on C++11 and Plain Old Data (POD) for more.

Expand to all classes
Think about the list of limitation that need to be qualified as aggregate in Aggregate on C++11. C++11 has removed all of them, for instance virtual function, base class with non-static member variables and so on.

// Example 4: C++11 initializer list
//********************************************************************************
struct Foo{
    Foo() = default;
Foo(int x, int y) : m_x{ x }, m_y{ y } {}

int m_x{ 1 };
int m_y{ 2 };

virtual int GetValue() const { return m_x + m_y; }
};

struct MyFoo : public Foo {
    MyFoo() = default;
MyFoo(int x, int y, int z) : Foo{ x, y }, m_z{ z } {}
int m_z { 3 };

virtual int GetValue() const { return Foo::GetValue() + m_z; }
};

void Test() {
Foo f{ 2, 3 }; // OK
MyFoo mf{ 4, 5, 6 }; // Ok

Foo fArr[3] = { Foo(1, 2), Foo{ 2, 3 }, Foo{ 4, 5 } }; // Ok
     MyFoo mfArr[3]; // Ok - use default
}
//********************************************************************************

Example 4 shows that C++11 nearly breaks all the crucial requirements needed for C++03 in order to qualify as aggregate then to be initialized by initializer list. Classes with virtual function, user-defined constructors and base class can work fine in all 3 cases in Example 2 shown in Aggregate on C++11. What an improvement!!!

Initializer_list constructor
C++11 provides a template class initializer_list as,
    namespace std {
        template<class E> class initializer_list;
    }
More details please refer to C++11 standard. This is provided in header <initializer_list> under namespace of std. It will create an array of const object E, "const E", with the size of the count of the objects appearing in the list. And provides interface to retrieve the head, tail and the size of the array with constant time complexity.

A constructor of classes with only argument of initializer_list or with the first argument of initializer_list and the rest with default value is called initializer_list constructor. It does not matter with any cv-qualifier.

// Example 5: initializer_list constructor
//********************************************************************************
struct Foo {
    Foo(const std::initializer_list<int>); // initializer_list constructor
    Foo(const std::vector<int>& ); //
};

Foo f({1, 2, 3}); // Ok - use initializer_list constructor
Foo f(std::vector<int>{1, 2, 3}); // Ok - use the 2nd constructor
//********************************************************************************

Quick quiz? How many types of constructors have you encounter so far?
    - default constructor
    - creation constructor
    - conversion constructor
    - copy constructor
    - move constructor
    - initializer_list constructor

Work on STL containers
Now in C++11 all STL containers have a initializer_list constructor. This means that all of them can be initialized directly via {}-list. See Example 6 how the std::vector can be easily initialized.

// Example 6: STL containers initialized via {}-list
//********************************************************************************
std::vector<int> myVec = {3, 7, 6, 5, 11};
std::vector<std::vector<int>> myVec2 = { {1, 3, 5},
                                                                {3, 7, 6, 5, 11},
                                                                {6, 4, 5, 2}};
//********************************************************************************

Let's look back the problem in Example 2 and look at how it is easily resolved in Example 6. Nicely done!!!

"Fix" most vexing parse problem
C++11 can not really wipe out this issue completely. If still written the same code as shown Example 3. The syntactic ambiguity issue will still surface. But C++11 provides an alternative to prevent this issue, which is to enable initializer-list to work on all class initialization. Therefore reduce the usage of "()", then work around this issue. This problem can be circumvented via using "{}"

// Example 7: workaround of most vexing parse problem in C++11
//********************************************************************************
Bar my_bar{Foo()};
//********************************************************************************

C++11 advises that prefer using "{}" to initialize the objects. In Example 7 it is clearly expressing the programmer's intention to create an object, because "{}" cannot be used to invoke a function and only "()" can do.

{}-list works everywhere
{}-list can be used to initialized everything now. In Example 4 default value for member variables, base class in initialization list, member variables in initialization list, objects and arrays. In Example 5 initializer_list's instantiation, initializer_list<int>. In Example 6 std::vector, one dimension and multiple-dimensions. In Example 7 object creation in constructor. More than these,
    - return type
    - work with new expression
    - function argument

// Example 8: {}-list
//********************************************************************************
struct Foo {
    Foo(int, int);
};

Foo CreateConstFooInstance() {
    return {1, 2};
}

int* intPtr = new int[4] {1, 2, 3, 4};

void Test(std::initializer_list<int>) {
    //......
}

Test({}); // Ok - empty list
Test({1, 2, 3, 4}; //Ok - 4 elements
//********************************************************************************

3. Summary
Comparing with C++03, C++11 has hugely expanded the functionality of initializer-list. C++11 has expanded initializer-list to all standard classes and STL containers. But the back-compatibility on aggregate and POD is maintained.  I think since now it becomes good practice that using {}-list to initialize variable and objects. At the same time limit the usage of "()" for variable/object initialization and use it solely for function/operator calling.

Bibliography:
[1] C++03 standard
[2] C++11 standard
[3] http://en.wikipedia.org/wiki/Most_vexing_parse
[4] http://www.stroustrup.com/C++11FAQ.html
[5] http://en.wikipedia.org/wiki/C++11
[6] [N2640=08-0150] Jason Merrill and Daveed Vandevoorde: Initializer Lists -- Alternative Mechanism and Rationale (v. 2)

No comments:

Post a Comment