std::initializer_list in C++ 1/2 – Internals and Use Instances

Programmer's Academy

In C++11, we acquired a helpful method to initialize varied containers. Reasonably than utilizing push_back() or insert() a number of instances, you possibly can leverage a single constructor by taking an initializer checklist. For instance, with a vector of strings, you possibly can write:

std::vector<std::string> vec { "abc", "xyz", "***" };

We are able to additionally write expressions like:

for (auto x : {1, 2, 3}) cout << x << ", ";

The above code samples use std::initializer_list and (some compiler assist) to carry the values and move them round.

Let’s perceive the way it works and what are its widespread makes use of.

That is the primary a part of initalizer_list mini-series. See the second half on Caveats and Enhancements right here.

Intro to the std::initializer_list

 

std::initializer_list<T>, is a light-weight proxy object that gives entry to an array of objects of sort const T.

The Normal reveals the next instance decl.init.checklist:

struct X {
  X(std::initializer_list<double> v);
};
X x{ 1,2,3 };

The initialization might be applied in a approach roughly equal to this:

const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));

In different phrases, the compiler creates an array of const objects after which passes you a proxy that appears like an everyday C++ container with iterators, start(), finish(), and even the dimension() operate. Right here’s a primary instance that illustrates the utilization of this sort:

#embrace <iostream>
#embrace <initializer_list>

void foo(std::initializer_list<int> checklist) {
    if (!std::empty(checklist)) {    
        for (const auto& x : checklist)
            std::cout << x << ", ";
        std::cout << "(" << checklist.dimension() << " components)n";        
    }
    else
        std::cout << "empty checklistn";
}

int primary() {
    foo({});
    foo({1, 2, 3});
    foo({1, 2, 3, 4, 5});
}

Run @Compiler Explorer

The output:

empty checklist
1, 2, 3, (3 components)
1, 2, 3, 4, 5, (5 components)

Within the instance, there’s a operate taking a std::initializer_list of integers. Because it appears like an everyday container, we are able to use non-member capabilities like std::empty, use it in a range-based for loop, and examine its dimension(). Please discover that there’s no have to move const initializer_list<int>& (a const reference) because the initializer checklist is a light-weight object, so passing by worth doesn’t copy the referenced components within the “hidden” array.

We are able to additionally reveal how the compiler sees the lists utilizing C++ Insights:

int primary()
{
  foo(std::initializer_list<int>{});
  const int __list52[3]{1, 2, 3};
  foo(std::initializer_list<int>{__list52, 3});
  const int __list134[5]{1, 2, 3, 4, 5};
  foo(std::initializer_list<int>{__list134, 5});
  return 0;
}

This time we have now three separate arrays.

Notice that we can’t do the identical with std::array because the parameter to a operate must have a hard and fast dimension. initializer_list has a variable size; the compiler takes care of that. Furthermore, the “inner” array is created on the stack, so it doesn’t require any extra reminiscence allocation (like should you used std::vector).

The checklist additionally takes homogenous values, and the initialization disallows narrowing conversions. For instance:

// foo({1, 2, 3, 4, 5.5}); // error, narrowing
foo({1, 'x', '0', 10}); // wonderful, char transformed to int

The textual content is predicated on my latest e-book: C++ initialization story. Test it out @Leanpub.

There’s additionally a helpful use case the place you should use range-based for loop instantly with the initializer_list:

#embrace <iostream>

int primary() {
    for (auto x : {"hiya", "coding", "world"})
        std::cout << x << ", ";
}

We are able to use the magic of C++ Insights and broaden the code to see the complete compiler transformation, see right here:

#embrace <iostream>

int primary()
{
  {
    const char *const __list21[3]{"hiya", "coding", "world"};
    std::initializer_list<const char *> && __range1 
                            = std::initializer_list<const char *>{__list21, 3};
    const char *const * __begin1 = __range1.start();
    const char *const * __end1 = __range1.finish();
    for(; __begin1 != __end1; ++__begin1) {
      const char * x = *__begin1;
      std::operator<<(std::operator<<(std::cout, x), ", ");
    }
    
  }
  return 0;
}

First, the compiler creates an array to carry string literals, then the __range, which is an initializer_list, after which makes use of an everyday range-based for loop.

Let’s now take a look at some use instances for this sort.

Use instances

 

From my observations, there are 4 main use instances for initializer_list:

  • creating container-like objects
  • implementing customized container-like objects
  • utilities like printing/logging
  • take a look at code

As typical, I attempted to get your assist and see your concepts:

Let’s have a better have a look at some examples.

Creating containers

 

All container courses from the Normal Library are geared up with the assist for initializer_list:

// vector
constexpr vector( std::initializer_list<T> init,
                  const Allocator& alloc = Allocator() );
                  
// map:
map( std::initializer_list<value_type> init,
     const Allocator& );
     
// ...     

And that’s why we are able to create new objects very simply:

std::vector<int> nums { 1, 2, 3, 4, 5 };
std::map<int, std::string> mapping {
        { 1, "one"}, {2, "two"}, {3, "three"}
};
std::unordered_set<std::string> names {"smith", "novak", "doe" };

Whereas the syntax is handy, some additional momentary copies may be created. We’ll deal with this problem within the subsequent article.

Array 2D

 

Normal containers are usually not particular; you may also implement such containers by yourself. For instance, I’ve discovered this instance for Array2d in TensorFlow repository:

// For instance, {{1, 2, 3}, {4, 5, 6}} leads to an array with n1=2 and n2=3.
Array2D(std::initializer_list<std::initializer_list<T>> values)
      : Array<T>(values) {}

Aliases

 

And one other instance the place an object can take a number of values by initializer_list, within the Krom venture:

void AddAlias(const char* from, const char* to);
void AddAlias(const char* from, const std::vector<std::string>& to);
void AddAlias(const char* from, const std::initializer_list<std::string>& to);

Notice that initializer_list will take priority over std::vector overload. We are able to present this with the next instance @Compiler Explorer:

#embrace <iostream>
#embrace <initializer_list>
#embrace <vector>

void foo(std::initializer_list<int> checklist) {
    std::cout << "checklist...n";
}

void foo(const std::vector<int>& checklist) {
    std::cout << "vector...n";
}

int primary() {
    foo({});
    foo({1, 2, 3});
    foo({1, 2, 3, 4, 5});
    std::vector<int> temp { 1, 2, 3};
    foo(temp);
    foo(std::vector { 2, 3, 4});
}

The output:

checklist...
checklist...
checklist...
vector...
vector...

As you possibly can see, passing {...} will choose the initializer_list model until we give a “actual” vector or assemble it explicitly.

A Difficult case

 

I additionally discovered some code within the Libre Workplace repository:

// ...
static const std::initializer_list<OUStringLiteral> vExtensions
        = { "gif", "jpg", "png", "svg" };

OUString aMediaDir = FindMediaDir(rDocumentBaseURL, rFilterData);
for (const auto& rExtension : vExtensions)
// ...    

vExtensions appears like a static assortment of string literals. However solely the initializer_list might be static, not the objects themselves!

For instance, this:

foo() {
    static const std::initializer_list<int> ll { 1, 2, 3 };
}

Expands to:

foo() {
    const int __list15[3]{1, 2, 3};
    static const std::initializer_list<int> ll =  std::initializer_list<int>{__list15, 3};
}

In that case, it’s greatest to make use of easy std::array because the compiler can deduce the variety of objects with none points:

static const std::array nums { 1, 2, 3 };

However possibly that’s extra on the “caveats” aspect…

Abstract

 

On this textual content, we lined some fundamentals and internals of std::initializer_list.

As we noticed, this wrapper sort is just a skinny proxy for a compiler-created array of const objects. We are able to move it round, iterate with a range-based for loop, or deal with it like a “view” container sort.

Within the following article, we’ll have a look at some caveats for this sort and search some enhancements. Keep tuned 🙂

Again to you

  • Do you write constructors or capabilities taking initializer_list?
  • Do you like container initialization with initializer_list or common push_back() or emplace_back()?

Share your feedback under.



(Visited 10 times, 1 visits today)

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
Ask ChatGPT
Set ChatGPT API key
Find your Secret API key in your ChatGPT User settings and paste it here to connect ChatGPT with your Tutor LMS website.
0
Would love your thoughts, please comment.x
()
x