Friday 6 June 2014

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

I have discussed the significant improvements of C++11 about initializer-list on C++11 - Initializer-list (Part I). Improvements  like workaround of the most vexing parse problem, initializing standard classes via {}-list, initializing STL containers via {}-list and so on.

From now on in C++11 it will be good practice to initialize variables/objects via {}-list and limit the use of "()" for invoking function and operator. Here I would like to list a few tips for correctly using initializer-list in C++11. Hopefully it will help fellow programmers in their daily job.

1. Overloading among constructors and functions
As I introduced in C++11 - Initializer-list (Part I), std::initialzier_list can be used everywhere for instance argument for function and initializer_list constructor. How does it co-exist with other functions or constructor and which one to invoke when overloading exists? Let's see Example 1.

// Example 1: overloading when std::initializer_list as argument
//********************************************************************************
struct Foo {
    Foo(std::initializer_list<int>};
    Foo(int);
    Foo(int, int);
    Foo(int, int, int);
};

Foo f1(1); // Foo(int)
Foo f2(1, 2); // Foo(int, int)
Foo f3(1, 2, 3); // Foo(int, int, int)
Foo f4{}; // Foo(std::initializer_list<int>};
Foo f5{1}; // Foo(std::initializer_list<int>};
Foo f6{1, 2}; // Foo(std::initializer_list<int>};
Foo f7{1, 2, 3}; // Foo(std::initializer_list<int>};
Foo f8(1, 2, 3, 4}; // Foo(std::initializer_list<int>};

struct MyFoo {
    MyFoo(std::initializer_list<int>);
    MyFoo(std::initializer_list<double);
};

MyFoo mf1{1, 2, 3}; // Ok: MyFoo(std::initializer_list<int>);
MyFoo mf2{1.0, 2.0, 3.0}; // Ok: MyFoo(std::initializer_list<double>);
MyFoo mf3{1, 2.0, 3.0}; // Error:: narrowing
MyFoo mf4{}; // Error: ambiguity
MyFoo mf5{std::initializer_list<double> {1, 2.0, 3.0}}; // Ok: explicit calling
MyFoo mf5{std::initializer_list<int> {1, 2.0, 3.0}}; // Error: narrowing

void Bar(std::initializer_list<int>);
void Bar(std::vector<int>);

Bar{}; // Error: - missing function calling "()"
Bar({}); // Ok - Bar(std::initializer_list<int>);
Bar({1, 2}) ; //Ok - Bar(std::initializer_list<int>);
Bar(std::vector<int>{1, 2}); // void Bar(std::vector<int>);
//********************************************************************************

The rule is that the function with std::initializer_list or initializer_list constructor will be selected/invoked if calling explicitly via {}-list, given that fact that the calling is legal and there is no narrowing existing in the list (Please refer to Section 3). There is a surprise for me at the first place as marked in red color in Example 1. I thought it should work and invoke "MyFoo(std::initializer_list<double>);", but unfortunately VC12 (Microsoft Visual Studio Express 2013) complains about it. Later on I kept reading the standard and found that this is due to the auto instantiation type deducing failure, as shown in Section 6.

2. Initializer-list mixed with auto
There is no forward declaration of std::initializer_list, therefore any explicit and implicit use of std::initializer_list will need to include header <initializer_list>. Using with "auto" is an implicit user case, so please remember to include the header.

Example 2: initializer-list with auto
//********************************************************************************
struct Foo {
    Foo(int, int);
};

void Test() {
auto MyVecFoo = { Foo(1, 2), Foo(2, 3), Foo(4, 5) };
for (auto iter = MyVecFoo.begin(); iter != MyVecFoo.begin(); ++iter) {
            // ....
}
}

void Bar(std::initializer_list<Foo> fooVec) {
    for (auto iter = fooVec.begin(); iter != fooVec.end(); ++iter) {
        // ......
    }
}
//********************************************************************************

auto is a new feature introduced in C+11. It enables the auto type deducing. For sure it will make template programming more pleasant.

3. Narrowing
There two types of narrowing. One is between build-in types and another is between derived-to-base objects. In C++11 neither of them is allowed with some exceptions in build-in types narrowing, when variables/objects are initialized via {}-list.

Example 3: narrowing between build-in types
//********************************************************************************
struct Foo {
    int x;
    double y;
    double z;
};

Foo f1{1, 2, 3.0}; // Ok: (2) int -> double
Foo f2{1.0, 2.0, 3.0}; // Error: narrowing (1.0) double -> int
Foo f3(1.0, 2.0, 3.0); .// Warning but Ok
Foo f4{}; // Ok: default value {0, 0.0, 0.0}

int x1{1}; //Ok
int x2{2.0}; // Error: narrowing
int x3 = 2.0; // Warning but Ok
int x4(2.0); // Warning but Ok

// the rest is from C++11 standard
int x = 999; // x is not a constant expression
const int y = 999;
const int z = 99;
char c1 = x; // OK, though it might narrow (in this case, it does narrow)
char c2{x}; // error: might narrow
char c3{y}; // error: narrows (assuming char is 8 bits)
char c4{z}; // OK: no narrowing needed
unsigned char uc1 = {5}; // OK: no narrowing needed
unsigned char uc2 = {-1}; // error: narrows
unsigned int ui1 = {-1}; // error: narrows
signed int si1 = { (unsigned int)-1 }; // error: narrows
int ii = {2.0}; // error: narrows
float f1 { x }; // error: might narrow
float f2 { 7 }; // OK: 7 can be exactly represented as a float
int f(int);
int a[] = { 2, f(2), f(2.0) }; // OK: the double-to-int conversion is not at the top level
//********************************************************************************

As shown in the red color in Example 3 the {}-list has tighter restriction than initializing via "()". As stated at the beginning of this blog, prefer {}-list to initialize values/objects over "()", because "()" could bury the potential numerical bugs in the code.

Here is the list of narrowing in C++11 between build-in types,
    - floating point type -> integer type
    - long double -> double/float; double -> float (Exception const value/expression converted to narrowed type without accuracy loss)
    - integer type/un-scoped enumeration type -> float point type (Exception const value/expression converted to float point type without accuracy loss and can be re-converted back to integer type/un-scoped enumeration type without accuracy loss)
    - integer type/un-scoped enumeration type -> narrowed integer type, for instance int -> char. (Exception const value/expression converted to narrowed integer type without accuracy loss and can be re-converted back to integer type/un-scoped enumeration type without accuracy loss)

Example 4: narrowing between user-defined types
//********************************************************************************
struct Base {
int x[8];
};

struct Derived : public Base {
int y[8];
};

Base myBaseArr[] = { Base{}, Derived{} }; // Ok
Derived myDerivedArr[] = { Derived{}, Base{} }; // Error: narrowing

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;

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

Foo fArr[4] = { Foo(), Foo{ 2, 3 }, Foo{ 4, 5 }, MyFoo{ 1, 2, 3 } }; // Ok
MyFoo mfArr[3] = { Foo{ 1, 2 }, MyFoo{ }, MyFoo{ 2, 3, 4 }}; // Error: narrowing
//********************************************************************************

There should be no surprise that base-to-derived does not work (regarded as narrowing) and however derived-to-base works.

4. Initialization of special types: reference and pointer
There is a bit of confusion of reference initialization when using with {}-list. Whether the reference variable/object can be initialized via {}-list depends on if it is a pure rvalue (can only appear in the right-hand side of operator). If they is a pure rvalue (prvalue) type, then it can be initialized via {}-list. Otherwise if it is a lvalue (can appear in the left-hand side of operator), then the answer is no.
Pointer can be initialized just like any other build-in types. It can be initialized with a combination of new expression plus {}-list for default value. And the default value of pointer is nullptr (see more on C++11 features - nullptr).

Example 5: initializer-list with reference and pointer
//********************************************************************************
const int& x = {1}; // Ok: x is a prvalue and bound to a temporary int
int& y = {1}; // Error: y is a lvalue hence it has to bound with a variable
const int (&intArr)[2] = {1, 2}; // Ok: const array and bound to a temporary int array

struct Foo {
    int x {1};
    std::string name{"Foo"};
};

const Foo& cfRef = {1, "My Foo"}; // Ok
Foo& fRef = {1, "My Foo"}; // Error

int* intPtr = new int[4] {1, 2, 3, 4}; // Ok: to allocate the memory and initialize the value
int* intPtr1{}; // Ok: default value - nullptr
iint** intPtr2{}; // Ok: default value - nullptr
//********************************************************************************

5. Internal implementation of std::initializer_list<T>
Internally std::initializer_list<T> can be treated as an implementation of wrapping an array and returning the head, tail and size.

// Example 6: Internal implementation of std::initializer_list (from C++11 standard with my favorite name)
//********************************************************************************
struct Foo {
    Foo(std::initializer_list<double>);
};

Foo f{1.0, 2.0, 3.0};
// equivalent to the following implementation internally
double foo_array[] = {1.0, 2.0, 3.0}
Foo f(std::intializer_list<double>(foo_array, foo_array+3));
//********************************************************************************

The life time of the array "foo_array" is the same as the object of std::initializer_list.

//Example 7: the life time of the internal array (from C++11 standard)
//********************************************************************************
typedef std::complex<double> cmplx;
std::vector<cmplx> v1 = { 1, 2, 3 };
void f() {
    std::vector<cmplx> v2{ 1, 2, 3 };
    std::initializer_list<int> i3 = { 1, 2, 3 };
}
//********************************************************************************

An array object is created for v1 and v2, and the array have the same life span with v1 and v2. But for i3 the array is an automatic object. And how many copies of this array are created depends on the implementation of the vendor of the compiler. The compiler could generate one copy of array of {1, 2, 3} in the read-only memory for v1, v2 and i3. Or it could generate one copy for each. Think about the implementation of literal string.

6. Deducing rules with template
As I introduced in C++11 - Initializer-list (Part I), std::initializer_list is a template class. Its type deducing rules comply with template class deducing rules. With the introduction of auto in C++11, std::initializer_list can be initialized as an auto object. Then the instantiation of the exact type of std::initializer_list has to be deduced at compiling time. If more than multiple types can be deduced and cause narrowing problem as introduced in Section 3, the compiler will complain.

// Example 8: auto deducing the instantiation type of std::initializer_list
//********************************************************************************
auto intIL = {1, 2, 3}; // Ok: all elements are in the same type "int"
auto errorIL = {1, 2.0, 3.0}; // Error: in VC12
/*
cannot deduce type for 'auto' from 'initializer-list'
Element '2': conversion from 'double' to 'int' requires a narrowing conversion
Element '3': conversion from 'double' to 'int' requires a narrowing conversion
*/
auto errorIL2 = { 1.0, 2, 3 }; // Error in VC12
/*
cannot deduce type for 'auto' from 'initializer-list'
Element '1': conversion from 'double' to 'int' requires a narrowing conversion
*/
//********************************************************************************

As well please see how the deducing rules cause the constructor/function calling failure in Example 1. Keep in mind that there should be no narrowing existing between elements, shown in Seciton 3, when initializing a std::initializer_list.

Summary
Use initializer-list to initialize variables/objects anywhere you can and use "()" for invoking function and operator only. And keep in mind the narrowing rules then you would be able to use it safely and smoothly.

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

No comments:

Post a Comment