std::initializer_list in C++ 2/2 – Caveats and Enhancements

[ad_1]

On this article, you’ll study why std::initializer_list has a nasty fame in C++. Is passing values utilizing is as environment friendly as “emplace”, how can we use non-copyable sorts? You’ll additionally see repair among the issues.

Let’s begin with the problems first:

1. Referencing native array

 

In case you recall from the earlier article, std::initializer_list expands to some unnamed native array of const objects. So the next code may be tremendous unsafe:

std::initializer_list<int> improper() { // for illustration solely!
    return { 1, 2, 3, 4};
}
int important() {
    std::initializer_list<int> x = improper();
}

The above code is equal to the next:

std::initializer_list<int> improper() {
    const int arr[] { 1, 2, 3, 4}
    return std::initializer_list<int>{arr, arr+4};
}
int important() {
    std::initializer_list<int> x = improper();
}

The instance serves to focus on the error, emphasizing the significance of avoiding related errors in our personal code. The perform returns pointers/iterators to an area object, and that can trigger undefined conduct. See a demo @Compiler Explorer.

GCC or Clang stories a helpful warning on this case:

GCC:
warning: returning non permanent 'initializer_list' doesn't lengthen the lifetime of the underlying array [-Winit-list-lifetime]
    5 |     return { 1, 2, 3, 4};

Or in Clang:

<supply>:5:12: warning: returning deal with of native non permanent object [-Wreturn-stack-address]
    return { 1, 2, 3, 4};

We will make the next conclusion:

std::initializer_list is a “view” kind; it references some implementation-dependent and an area array of const values. Use it primarily for passing into capabilities if you want a variable variety of arguments of the identical kind. In case you attempt to return such lists and go them round, then you definitely danger lifetime points. Use with care.

Let’s tackle one other limitation:

2. The price of copying components

 

Passing components via std::initializer_list could be very handy, however it’s good to know that if you go it to a std::vector’s constructor (or different customary containers), every factor must be copied. It’s as a result of, conceptually, objects within the initializer_list are put right into a const non permanent array, so that they must be copied to the container.

Let’s evaluate the next case:

std::cout << "vec... { }n";
std::vector<Object> vec { Object("John"), Object("Doe"), Object("Jane") };

With:

std::cout << "vec emplace{}n";
std::vector<Object> vec;
vec.reserve(3);
vec.emplace_back("John");
vec.emplace_back("Doe");
vec.emplace_back("Jane");

The Object class is a straightforward wrapper round std::string with some further logging code. You’ll be able to run the instance @Compiler Explorer.

And we’ll get:

vec... { }
MyType::MyType John
MyType::MyType Doe
MyType::MyType Jane
MyType::MyType(const MyType&) John
MyType::MyType(const MyType&) Doe
MyType::MyType(const MyType&) Jane
MyType::~MyType Jane
MyType::~MyType Doe
MyType::~MyType John
MyType::~MyType John
MyType::~MyType Doe
MyType::~MyType Jane
vec emplace{}
MyType::MyType John
MyType::MyType Doe
MyType::MyType Jane
MyType::~MyType John
MyType::~MyType Doe
MyType::~MyType Jane

As you possibly can see, one thing is improper! Within the case of the one constructor, we are able to spot some further non permanent objects! However, the case with emplace_back() has no temporaries.

We will hyperlink this problem with the next factor:

3. Non-copyable sorts

 

The additional copy that we’d get with the initializer_list, additionally causes points when your objects should not copyable. For instance, if you wish to create a vector of unique_ptr.

#embody <vector>
#embody <reminiscence>

struct Form {    digital void render() const ; };
struct Circle : Form { void render() const override; };
struct Rectangle : Form {  void render() const override; };

int important() {
    std::vector<std::unique_ptr<Form>> shapes {
        std::make_unique<Circle>(), std::make_unique<Rectangle>()
    };
}

Run @Compiler Explorer

The road the place I wish to create a vector fails to compile, and we get many messages about copying points.

Briefly: the distinctive pointers can’t be copied. They will solely be moved, and passing initializer_list doesn’t give us any choices to deal with these circumstances. The one technique to construct such a container is to make use of emplace_back or push_back:

std::vector<std::unique_ptr<Form>> shapes;
shapes.reserve(2);
shapes.push_back(std::make_unique<Circle>());           // or
shapes.emplace_back(std::make_unique<Rectangle>());

See the working code at Compiler Explorer.

Various: variadic perform

 

If you wish to cut back the variety of non permanent objects, or your sorts should not copyable, then you need to use the next manufacturing facility perform for std::vector:

template<typename T, typename... Args>
auto initHelper(Args&&... args) {
    std::vector<T> vec;
    vec.reserve(sizeof...(Args)); 
    (vec.emplace_back(std::ahead<Args>(args)), ...);
    return vec;
}

See at @Compiler Explorer

After we run the code:

std::cout << "initHelper { }n";
auto vec = initHelper<Object>("John", "Doe", "Jane");

We’ll get the next:

initHelper { }
MyType::MyType John
MyType::MyType Doe
MyType::MyType Jane
MyType::~MyType John
MyType::~MyType Doe
MyType::~MyType Jane

What’s extra, the identical perform template can be utilized to initialize from moveable objects:

std::vector<std::unique_ptr<Form>> shapes;
shapes.reserve(2);
shapes.push_back(std::make_unique<Circle>());
shapes.emplace_back(std::make_unique<Rectangle>());

auto shapes2 = initHelper<std::unique_ptr<Form>>(
     std::make_unique<Circle>(), std::make_unique<Rectangle>());

Run at @Compiler Explorer

Core options:

  • Variadic templates permit us to go any variety of arguments right into a perform and course of it with argument pack syntax.
  • (vec.emplace_back(std::ahead<Args>(args)), ...); is a fold expression over a comma operator that properly expands the argument pack at compile time. Fold expressions have been accessible since C++17.

Extra proposals

 

The initializer record just isn’t good, and there have been a number of makes an attempt to repair it:

From the paper N4166 by David Krauss

std::initializer_list was designed round 2005 (N1890) to 2007 (N2215), earlier than transfer semantics matured, round 2009. On the time, it was not anticipated that duplicate semantics can be inadequate and even suboptimal for widespread value-like courses. There was a 2008 proposal “N2801 Initializer lists and transfer semantics” however C++0x was already felt to be slipping at the moment, and by 2011 the case had gone chilly.

There’s additionally a proposal by Sy Model: WG21 Proposals – init record

Nonetheless, as of 2022, there have but to be any enhancements applied in the usual. So we have now to attend a bit.

Lately there’s additionally a proposal from Arthur: P1967 `#embed` and D2752 “Static storage for `initializer_list`” are actually on Compiler Explorer. That is very attention-grabbing because it permits us to place that unnamed const array into the static storage quite than on the stack. We’ll see the way it goes.

Abstract

 

Within the article, I confirmed you three points with initializer_list: further copies, lack of help for non-copyable objects, and undefined conduct when returning such lists. The case with non-copyable objects might be solved through the use of a comparatively easy manufacturing facility perform that accepts a variadic pack.

Sources

 

Again to you

  • Do you like container initialization with initializer_list or common push_back() or emplace_back()?
  • Do you are inclined to optimize and cut back non permanent copies when doable?

Share your feedback beneath.

[ad_2]

Leave a Reply

Your email address will not be published. Required fields are marked *