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 ofX
to perform the initialization of an object of typeX
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 argumentsi
,ICSi(F1)
is not a worse conversion sequence thanICSi(F2)
, and then:(1.3) — for some argument
j
,ICSj(F1)
is a better conversion sequence thanICSj(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
Convert a Single Color with Cvtcolor
Treating Memory Returned by Operator New(Sizeof(T) * N) as an Array
Global Function Definition in Header File - How to Avoid Duplicated Symbol Linkage Error
Allocating a Large Memory Block in C++
C++ Sizeof(Array) Return Twice the Array's Declared Length
Recommended Values for Opencv Detectmultiscale() Parameters
Generating a Normal Map from a Height Map
In C++, What Does & Mean After a Function's Return Type
Is There Any Alternative to Using % (Modulus) in C/C++
C++11 Way to Index Tuple at Runtime Without Using Switch
Is There Any Lame C++ Wrapper\Simplifier (Working on Linux MAC and Win from Pure Code)
"Launch Failed. Binary Not Found." Snow Leopard and Eclipse C/C++ Ide Issue
How to Print Utf-8 Strings to Std::Cout on Windows
Understanding the Dangers of Sprintf(...)
Checking Value Exist in a Std::Map - C++
Conflict Between Dynamic Linking Priority in Osx
Do I Need to Protect Read Access to an Stl Container in a Multithreading Environment