list initialization and std::initializer_list

C++, C++11, initializer_list, emplace, emplace_back, list initialization

time: 2019-03-10-Sun 02:29:50

List initialization, since C++11

List initialization is a “new” syntax support (sugar) since C++11, the main idea is to initialize object with a given list of arguments in enclosed brace for initialization.

direct-list-initialization

T object { arg1, arg2, ... };                  (1)
T { arg1, arg2, ... }                          (2)
new T { arg1, arg2, ... }                      (3)
Class { T member { arg1, arg2, ... }; };       (4)
Class::Class() : member{arg1, arg2, ...} {...  (5)

copy-list-initialization

T object = {arg1, arg2, ...};                  (6)
function( { arg1, arg2, ... } )                (7)
return { arg1, arg2, ... } ;                   (8)
object[ { arg1, arg2, ... } ]                  (9)
object = { arg1, arg2, ... }                   (10)
U( { arg1, arg2, ... } )                       (11)
Class { T member = { arg1, arg2, ... }; };     (12)

The rule is simple, order of arguments in braced-init-list match the corresponding constructor’s, compiler will pick the ctor according the given list.

For POD type (pure struct), there will be a default list-initialization constructor which tasks arguments as the member variable.

struct S {
  std::string str1;
  int a1;
  std::string str2;
  std::string str3;
};

S s1 {"abc", 1, "bbb", "ccc"}; // ok, will init in order
S s2 {"abc", 1, {"bbb"}, "ccc"}; // ok

Note that a braced-init-list is not an expression and therefore has no type, e.g. decltype({1,2}) is ill-formed, it can not be deduced when using a template.

For more explanation of list initialization, check this site

With this syntax support we can initialize a std::pair<std::string, int>

std::pair<std::string, int> p {"10086", 10010};

to initialize a std::vector<std::pair<std::string, int>> as

std::vector<std::pair<std::string, int>> v {
  {"10000", 200},
  {"10010", 300},
  {"10086", 400},
};

However, we can simplify initialization of std::vector<std::pair<std::string, int>> not only with help of list-initialization but also with help of std::initializer_list.

Initializer list

An object of type std::initializer_list is a lightweight proxy object that provides access to an array of objects of type const T.

A std::initializer_list object is automatically constructed when:

  • a braced-init-list is used to list-initialize an object, where the corresponding constructor accepts an std::initializer_list parameter
  • a braced-init-list is used as the right operand of assignment or as a function call argument, and the corresponding assignment operator/function accepts an std::initializer_list parameter
  • a braced-init-list is bound to auto, including in a ranged for loop

Initializer lists may be implemented as a pair of pointers or pointer and length. Copying a std::initializer_list does not copy the underlying objects.

The initializer_list template only accepts a type T, it’s usually used for container initialization.

std::vector has a constructor of std::initlizaer_list, possible initialization is

template<typename T>
std::vector(std::initializer_list<T> l) {
  reserve(l.size());
  for (auto& i : l) {
    emplace_back(i); // may be move here
  }
}

Usage of std::initializer_list

Essentially, std::initializer_list is a list of arguments we want to pass, the number of elements in initializer list is not fixed.

We can iterate the list over with iterator or range-for

void foo(std::initializer_list<std::string> l) {
  auto it = l.begin();
  while (it != l.end()) {
    std::cout << *it;
    ++it;
  }

  // or range for
  for (auto& i :  l) {
    std::cout << l;
  }
}

void foo(std::initializer_list<std::string> l) {
  if (l.size() < 5) { // l.size() is not a constexpr
    throw std::logic_error();
  }
}

One more thing to say, e.g., there is a declaration

Foo::Foo(int);

foo(1) and foo({1}) is the same to call that function, however this will cause gcc or clang to warn

braces around scalar initializer

You should remove the braces: { and } around single value, gcc/clang thinks it is verbose to do that for a single scalar type.

And the priority for matching is the same as that for overloading.

Foo::Foo(std::initializer_list<std::stirng>); // ctor2
Foo::Foo(std::string); // ctor3

It’s not ambiguous, a call of Foo(std::string) will match ctor3 exactly, no warning.

Explicitly declare type/use of initializer_list for template

The emplace or emplace_back for containers take arbitrary arguments for the element type constructor.

The common implementation, gcc or clang, is following (pseudo code)

template<>
vector.emplace_back(Args&&... args) {
  // placement_new and then
  ctor(std::forward<Args&&>(args)...);
}

// almost the same as empalce
template<>
set.emplace(Args&&... args)

There is an arguments forwarding, if we write down something like the following, the compiler will be confused when deduce the template for constructor, it keeps the {} thing, raw initialize_list <brace-enclosed initializer list>, all through the deduction, it has no type, which will not make any sense for deduction.

The simple one

// wont compile
std::vector<std::vector<int>> vv({1, 2, 3});

or, there is a more complicated one

struct Range {
  Range(std::initializer_list<std::string>) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
};

struct Tablet {
  Tablet(int64_t, const Range&, int64_t) { // ctor1
  }
  Tablet(int64_t, std::initializer_list<std::string>, int64_t) { // ctor2
  }
};

void test_emplace() {
  std::vector<Tablet> v;
  v.emplace_back(1, Range("10", "20"), 2); // ok, ctor1
  v.emplace_back(1, Range{"10", "20"}, 2); // ok, ctor1

  //  no matching function for call to
  //  ‘std::vector<Tablet>::emplace_back(int, <brace-enclosed initializer list>, int)’
  // v.emplace_back(1, {"10", "20"}, 2);

  // explicitly declare what is the init list for
  v.emplace_back(1, (std:initializer_list<std::string>){"10", "20"}, 2); // ok, ctor2
  v.emplace_back(1, (Range){"10", "20"}, 2); // ok, ctor1
  // or
  v.emplace_back(1, std:initializer_list<std::string> {"10", "20"}, 2); // ok, ctor2
  v.emplace_back(1, Range {"10", "20"}, 2); // ok, ctor1
}

We should explicitly “declare” what the <brace-enclosed initializer list> is in the template function, be aware of that the following statements are not type cast, it’s just like a “declaration” to specify the type/use of that initializer list.

The following is kind of “decalaration”, decalaring that <brace-enclosed initializer list> is a certain type.

(std:initializer_list<std::string>){"10", "20"}
(Range){"10", "20"}

The above code can be simply written as

std:initializer_list<std::string> {"10", "20"}
Range {"10", "20"}

Apparently, the following wont compile, it’s a syntax error, because {...} is being deduced without any context (type info), which will certainly fail.

// static cast
static_cast<std:initializer_list<std::string>>({"10", "20"})
static_cast<Range>({"10", "20"})

// force cast
(std:initializer_list<std::string>)({"10", "20"})
(Range)({"10", "20"})

Conclusion

  • list initialization is convenient for use
  • raw initializer list, A.K.A, braced-init-list has no type
  • do not pass a braced-init-list for template deduction, “assign” (declare) it a “type” explicitly
  • passing std::initializer_list by value is very cheap, don’t worry about performance

Reference

https://stackoverflow.com/questions/24550924/emplacement-of-a-vector-with-initializer-list
https://en.cppreference.com/w/cpp/language/list_initialization
https://en.cppreference.com/w/cpp/utility/initializer_list

braft call graph

This is the call graph of [braft](https://github.com/brpc/braft),an industrial grade raft implementation, which may help you understand b...… Continue reading

Clock And Timestamp

Published on August 16, 2019

memory ordering

Published on April 30, 2019