Why Isn't 'Std::Initializer_List' Defined as a Literal Type

Why isn't `std::initializer_list` defined as a literal type?

The standard committee seems to intend on initializer_list being a literal type. However, it doesn't look like it's an explicit requirement, and seems to be a bug in the standard.

From § 3.9.10.5:

A type is a literal type if it is:

- a class type (Clause 9) that has all of the following properties:

- - it has a trivial destructor,

- - it is an aggregate type (8.5.1) or has at least one constexpr constructor or constructor template that is not a copy or move constructor, and

- - all of its non-static data members and base classes are of non-volatile literal types.

From § 18.9.1:

namespace std {
template<class E> class initializer_list {
public:
/* code removed */
constexpr initializer_list() noexcept;
// No destructor given, so trivial
/* code removed */
};
}

This satisfies the first and second requirements.

For the third requirement though:

From § 18.9.2 (emphasis mine):

An object of type initializer_list<E> provides access to an array of objects of type const E. [Note: A pair of pointers or a pointer plus a length would be obvious representations for initializer_list. initializer_list is used to implement initializer lists as specified in 8.5.4. Copying an initializer list does not copy the underlying elements.

—end note]

So there is no requirement for the private members of the implementation of initializer_list to be non-volatile literal types; however, because they mention that they believe a pair of pointers or a pointer and a length would be the "obvious representation," they probably didn't consider that someone might put something non-literal in the members of initializer_list.

I'd say that it's both a bug in clang and the standard, probably.

Strange behavior of std::initializer_list of std::strings

It calls

string(const char* b, const char* e) 

string ctor overload.

It works only if b and e points to the same string literal. Otherwise it is undefined behaviour.

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.

Is there a way to create a user-defined-literal from `std::initializer_list`?

template<class T, std::size_t N>
struct any_of:std::array<T, N> {
#define MAKE_OPERATOR( OPERATOR ) \
template<class U, \
std::enable_if_t< std::is_same<void, std::void_t< \
decltype( std::declval<T const&>() == std::declval<U const&>() ) \
>>{}, bool> =true \
> \
friend bool operator OPERATOR ( any_of const& lhs, U const& rhs) { \
return std::any_of( \
lhs.begin(), lhs.end(), \
[&](auto&& lhs){ return lhs OPERATOR rhs; } \
); \
} \
template<class U, \
std::enable_if_t< std::is_same<void, std::void_t< \
decltype( std::declval<U const&>() == std::declval<T const&>() ) \
>>{} && !std::is_same< U, any_of >{} , bool> =true \
> \
friend bool operator OPERATOR ( U const& lhs, any_of const& rhs) { \
return std::any_of( \
rhs.begin(), rhs.end(), \
[&](auto&& rhs){ return lhs OPERATOR rhs; } \
); \
}
MAKE_OPERATOR(==)
MAKE_OPERATOR(!=)
MAKE_OPERATOR(<)
MAKE_OPERATOR(<=)
MAKE_OPERATOR(>=)
MAKE_OPERATOR(>)
#undef MAKE_OPERATOR
explicit any_of( std::array<T, N> arr):std::array<T, N>(std::move(arr)) {}
template<class...Ts>
explicit any_of( T t, Ts... ts ):std::array<T, N>{ std::move(t), std::move(ts)... } {}
any_of( any_of const& )=delete;
any_of& operator=( any_of const& )=delete;
any_of()=delete;
};
template<class T, std::size_t N>
any_of(T(&)[N]) -> any_of<T,N>;
template<class T, class...Ts>
any_of(T, Ts...) -> any_of<T, 1+sizeof...(Ts)>;

test code:

if (any_of{1,2,3} == 2) {
std::cout << "2 is there\n";
}
if (! (any_of{1,2,3} == 7) ){
std::cout << "7 is not there\n";
}

if (any_of{1,2,3} == any_of{5,6,1}) {
std::cout << "overlap!\n";
}
if (!(any_of{1,2,3} == any_of{5,6,7})) {
std::cout << "no overlap!\n";
}

Live example.

Output in c++17 compiler:

2 is there
7 is not there
overlap!
no overlap!

The various comparison operators are all supported.

Cross-type double any_of, like:

any_of{1,2,3} == any_of{3.14, 5.7, 1.0}

will fail to compile because both == of both any_of work.

Is it legal to declare a constexpr initializer_list object?

Update: The situation got a bit more complicated after the resolution of CWG DR 1684 removed the requirement quoted below. Some more information can be found in this discussion on the std-discussion mailing list and in the related question Why isn't `std::initializer_list` defined as a literal type?


[decl.constexpr]/8:

A constexpr specifier for a non-static member function that is not a constructor declares that member function to be const (9.3.1). [...] The class of which that function is a member shall be a literal type (3.9).

Therefore, the changes of N3471 guarantee std::initializer_list will be a literal type.


Note the constexpr ctor alone doesn't require std::initializer_list to be a literal type, see [dcl.constexpr]/4+8. Side note: An object of non-literal type with constexpr ctor can be initialized during constant initialization [basic.start.init]/2] (part of static initialization, performed before any dynamic initialization).

Why is the size not a template argument of std::initializer_list?

One upside of the existing system is that you can export functions which take an initializer_list from a DLL. If it were templated on the size, they would have to be shipped as source.

C++ std::initializer_list usage

1

So my first question is: the line above marked with (*), is there some kind of syntactic sugar behind that allows this syntax (or maybe regular syntax for the compiler, so that he mangles with it and sets up everything properly)?

From cppreference:

Notes

Despite a lack of constructors, it is possible to create non-empty initializer lists. Instances of std::initializer_list are implicitly constructed when:

  • a braced-init-list is used in list-initialization, including function-call list initialization and assignment expressions (not to be confused with constructor initializer lists)
  • a braced-init-list is bound to auto, including in a ranged for loop

2

Next question [...]: if I'm not explicitly using new initializer_list({...});, can the initializer_list ever use dynamic memory in any way (is there a certain use-case where it does)?

Why would you? Their main purpose is to pass a list of parameters to constructors. They are a lightweight wrapper around an array that is constructed by the compiler on the fly to allow you write code along the line of

std::string s1{'a', 'b', 'c', 'd'};

3a

In this case, is there any possible way to use the constructor of the Test class other than the way it is used with the t1 and t2 instances?

I do not really understand what you are asking here. I would write it like this:

Test t1{ 4, 6, 3 };
Test t2{ 6, 3, 2, 8 };

3b

And of course, is this even correct to begin with?

There is nothing wrong in your code.

3c

like for instance the 'brace init list' { ... } being destroyed (if saying something like this is even possible) before cout uses them?

Again from cppreference:

The underlying array is a temporary array of type const T[N], in which each element is copy-initialized (except that narrowing conversions are invalid) from the corresponding element of the original initializer list. The lifetime of the underlying array is the same as any other temporary object, except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary (with the same exceptions, such as for initializing a non-static class member). The underlying array may be allocated in read-only memory.

TL;DR: Even temporaries dont cease to exists between two random lines of code. You can safely use the function parameter lst till the end of the function.

PS

Only while half-way through this answer I realized that it is not only 3 but actually even more questions. Better focus on one point in a question. If you have more, open more questions.



Related Topics



Leave a reply



Submit