Why Is 'Std::Initializer_List' Often Passed by Value

why is `std::initializer_list` often passed by value?

It’s passed by value because it’s cheap. std::initializer_list, being a thin wrapper, is most likely implemented as a pair of pointers, so copying is (almost) as cheap as passing by reference. In addition, we’re not actually performing a copy, we’re (usually) performing a move since in most cases the argument is constructed from a temporary anyway. However, this won’t make a difference for performance – moving two pointers is as expensive as copying them.

On the other hand, accessing the elements of a copy may be faster since we avoid one additional dereferencing (that of the reference).

Why take a std::initializer_list by rvalue reference vs. by value?

I'm not sure I have a good reason, but my guess would be that the author of the code may have thought that passing an initializer list by value would make unnecessary copies of the elements in the list. This is, of course, not true - copying an initializer list does not copy the underlying values.

Why elements of std::initializer_list have to be copied?

"Copy initialization" in C++ doesn't mean things will necessarily be copied. It's just a formal name for the constraints under which initialization will occur. For instance, when copy initializing, explicit c'tors are not candidates. So the following code will be ill-formed

#include <iostream>
#include <initializer_list>

struct A {
explicit A() = default;
A(const A&){ std::cout << "Oh no, a copy!\n"; }
};
struct B { B(std::initializer_list<A> il); };

int main()
{
B b{ {} };
return 0;
}

The single member in the list needs to be copy-initialized from {}, which entails calling the default c'tor. However, since the c'tor is marked explicit, this initialization cannot happen.

Copy elision is certainly possible pre-C++17, and is mandatory in C++17 onward in certain contexts. In your example, under a C++17 compiler, since you provide an initializer that is a prvalue (a pure rvalue, not an object), the initialization rules of C++ mandate that the target is initialized directly, without intermediate objects created. Even though the context is called "copy initialization", there are no superfluous objects.

lifetime of a std::initializer_list return value

The wording you refer to in 8.5.4/6 is defective, and was corrected (somewhat) by DR1290. Instead of saying:

The lifetime of the array is the same as that of the initializer_list object.

... the amended standard now says:

The array has the same lifetime as any other temporary object (12.2 [class.temporary]), except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary.

Therefore the controlling wording for the lifetime of the temporary array is 12.2/5, which says:

The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement

Therefore the noisydt objects are destroyed before the function returns.

Until recently, Clang had a bug that caused it to fail to destroy the underlying array for an initializer_list object in some circumstances. I've fixed that for Clang 3.4; the output for your test case from Clang trunk is:

destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received

... which is correct, per DR1290.

How come std::initializer_list is allowed to not specify size AND be stack allocated at the same time?

The thing is, std::initializer_list does not hold the objects inside itself. When you instantiate it, compiler injects some additional code to create a temporary array on the stack and stores pointers to that array inside the initializer_list. For what its worth, an initializer_list is nothing but a struct with two pointers (or a pointer and a size):

template <class T>
class initializer_list {
private:
T* begin_;
T* end_;
public:
size_t size() const { return end_ - begin_; }
T const* begin() const { return begin_; }
T const* end() const { return end_; }

// ...
};

When you do:

foo({2, 3, 4, 5, 6});

Conceptually, here is what is happening:

int __tmp_arr[5] {2, 3, 4, 5, 6};
foo(std::initializer_list{arr, arr + 5});

One minor difference being, the life-time of the array does not exceed that of the initializer_list.

Why does C++ allow std::initializer_list to be coerced to primitive types, and be used to initialise them?

That {} syntax is a braced-init-list, and since it is used as an argument in a function call, it copy-list-initializes a corresponding parameter.

§ 8.5 [dcl.init]/p17:

(17.1) — If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).

§ 8.5.4 [dcl.init.list]/p1:

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is
called an initializer list, and the comma-separated initializer-clauses of the list are called the elements of the
initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; [...]

For a class-type parameter, with list-initialization, overload resolution looks up for a viable constructor in two phases:

§ 13.3.1.7 [over.match.list]/p1:

When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor
in two phases:

— Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.

— If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

but:

If the initializer list has no elements and T has a default constructor, the first phase is omitted.

Since std::deque<T> defines a non-explicit default constructor, one is added to a set of viable functions for overload resolution. Initialization through a constructor is classified as a user-defined conversion (§ 13.3.3.1.5 [over.ics.list]/p4):

Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single
best constructor of X to perform the initialization of an object of type X from the argument initializer list,
the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion
sequence an identity conversion.

Going further, an empty braced-init-list can value-initialize its corresponding parameter (§ 8.5.4 [dcl.init.list]/p3), which for literal types stands for zero-initialization:

(3.7) — Otherwise, if the initializer list has no elements, the object is value-initialized.

This, for literal types like bool, doesn't require any conversion and is classified as a standard conversion (§ 13.3.3.1.5 [over.ics.list]/p7):

Otherwise, if the parameter type is not a class:

(7.2) — if the initializer list has no elements, the implicit conversion sequence is the identity conversion.

[ Example:

void f(int);
f( { } );
// OK: identity conversion

end example ]

Overload resolution checks in first place if there exists an argument for which a conversion sequence to a corresponding parameter is better than in another overload (§ 13.3.3 [over.match.best]/p1):

[...] Given these definitions, a viable function F1 is defined to be a better function than another viable function
F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then:

(1.3) — for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that, [...]

Conversion sequences are ranked as per § 13.3.3.2 [over.ics.rank]/p2:

When comparing the basic forms of implicit conversion sequences (as defined in 13.3.3.1)

(2.1) — a standard conversion sequence (13.3.3.1.1) is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and [...]

As such, the first overload with bool initialized with {} is considered as a better match.

How are `std::initializer_list` values passed by the compiler? (Or: how can I get around a universal overload with one?)

Disambiguation cast:

int     f( int );
double f( double );

template < typename Func, typename ...Args >
void do_it( Func &&f, Args &&...a );

//...

int main( int, char *[] )
{
do_it( (double(*)(double))&f, 5.4 );
return 0;
}

The code in main is forced to use the second version of f. I thought this capability was unique to the C-cast, but it's under static_cast. So I got something like:

class array_md
{
//...
template < typename ...Index >
complicated & at( Index &&...i ); // (1)

template < typename ...Index >
complicated const & at( Index &&...i ) const; // (2)

my_type & at( std::initializer_list<size_type> i ) // (3)
{
return const_cast<my_type &>(
(
const_cast<array_md const *>( this )
->*
static_cast<
my_type const &
(array_md::*)
( std::initializer_list<size_type> ) const
>( &array_md::at )
)( i )
);
}

my_type const & at( std::initializer_list<size_type> i ) const; // (4)
//...
};

(I got my inspiration by answering someone else's post with needing to distinguish function template overloads.) It took a few tries, especially without having to create an intermediate object.

Constructor taking std::initializer_list is preferred over other constructors

The compiler is pretty much never "free to choose" for stuff like this. If it were, we wouldn't be able to write pretty much any portable C++ code.

[over.match.list] does give priority to initializer_list constructors. Constructor function overloading under the rules of list initialization gets invoked at step 3.6. Steps 3.1-3.5 do not apply, as your type doesn't qualify for any of those cases. Step 3.1 is particularly interesting, as it is specifically meant to invoke copy constructors instead of doing other things, but it also only applies to aggregates. Which your type is not.

Since your type is implicitly convertible to int, and your type takes an initializer_list<int>, there is a valid way to build an initializer_list that matches a constructor for the type in question. Therefore, this is the constructor [over.match.list] will select.

So in this case, VC++ is wrong. As is Clang, apparently.



Related Topics



Leave a reply



Submit