If I create an std::vector<int>
and initialize it two ways, both call the std::initializer_list
constructor.
std::vector<int> v1{1, 2, 3}; // Calls vector(initializer_list< int >)
std::vector<int> v2 = {1, 2, 3}; // Calls vector(initializer_list< int >)
But if I use a class that implicitly converts from int
:
struct Imp { Imp(int) {} };
Then the second method of initialization calls the constructor that takes a pair of iterators.
std::vector<Imp> v3{1, 2, 3}; // Calls vector(initializer_list< Imp >)
std::vector<Imp> v4 = {1, 2, 3}; // Calls vector(const int*, const int* ) ???
This only happens using GCC 14.2. When using Clang 19.1 or MSVC 19.40, both v3
and v4
call the initializer_list
constructor. Optimization level doesn't make a difference
Why does the = {}
syntax call vector(const int*, const int*)
on GCC?
I tried creating my own class template that has both constructors:
template <typename T>
struct Custom {
Custom(std::initializer_list<T>) {}
Custom(const int*, const int*) {}
};
Now both the {}
and = {}
syntax call the initializer_list
constructor on GCC.
Custom<Imp> v5{1, 2, 3}; // Calls Custom(initializer_list< Imp >)
Custom<Imp> v6 = {1, 2, 3}; // Calls Custom(initializer_list< Imp >)
Where things get really confusing, is if I specialize std::vector
for my implicitly-converting type Imp
. Then there are three cases:
Case 1 - Provide initializer_list
ctor only
template <>
struct std::vector<Imp> {
vector(initializer_list<Imp>) {}
};
Result - Both syntaxes call the provided ctor
std::vector<Imp> v7{1, 2, 3}; // Calls vector(initializer_list< Imp >)
std::vector<Imp> v8 = {1, 2, 3}; // Calls vector(initializer_list< Imp >)
Case 2 - Provide const int*
ctor only
template <>
struct std::vector<Imp> {
vector(const int*, const int*) {}
};
Result - Both syntaxes fail to compile (no matching ctor)
std::vector<Imp> v9{1, 2, 3}; // Error - no matching ctor
std::vector<Imp> vA = {1, 2, 3}; // Error - no matching ctor
Case 3 - Provide both ctors
template <>
struct std::vector<Imp> {
vector(initializer_list<Imp>) {}
vector(const int*, const int*) {}
};
Result - The {}
syntax calls the initializer_list
ctor, while the = {}
syntax calls the const int*
ctor
std::vector<Imp> vB{1, 2, 3}; // Calls vector(initializer_list< Imp >)
std::vector<Imp> vC = {1, 2, 3}; // Calls vector(const int*, const int*) ???
Here is where I am lost.
In case 3, how does providing the initializer_list
constructor allow the const int*
constructor to be called?
It was asked how I know which constructor is being called. This started when I was looking at when copy vs. move constructors are called. std::initializer_list
cannot be moved from. But here are some additional examples, with prints.
Example 1 - Using a standalone type
struct Imp { Imp(int) {} };
template <typename T>
struct Custom {
Custom(std::initializer_list<T>) { std::printf("initializer_list<T>\n"); }
Custom(const int*, const int*) { std::printf("const int*, const int*\n"); }
};
int main(int argc, char *argv[]) {
Custom<Imp> v{1, 2, 3};
Custom<Imp> w = {1, 2, 3};
Prints:
initializer_list<T>
initializer_list<T>
Example 2 - Using an std::vector specialization
struct Imp { Imp(int) {} };
template <>
struct std::vector<Imp> {
vector(initializer_list<Imp>) { std::printf("initializer_list<T>\n"); }
vector(const int*, const int*) { std::printf("const int*, const int*\n"); }
};
int main(int argc, char *argv[]) {
std::vector<Imp> v{1, 2, 3};
std::vector<Imp> w = {1, 2, 3};
Prints:
initializer_list<T>
const int*, const int*
The only difference between the two examples is that one is an std:: specialization, and the other is not.
Why would this change which ctor overload is called?
Congratulations, you just found a secret optimization done by GCC!
Since GCC 13, GCC rewrites certain list-initializations (which should call the initializer_list
constructor) to use the iterator-pair constructor instead. This is intended to work around the inefficiency when constructing a vector<string>
with a list of string literals. See GCC bug 105838.
This behavior is described in GCC's source code.
If we were going to call e.g. vector(initializer_list<string>) starting with a list of string-literals (which is inefficient, see PR105838), instead build an array of const char* and pass it to the range constructor. But only do this for standard library types, where we can assume the transformation makes sense.
As to why this optimization is applied to std::vector<T> v = {1, 2, 3}
but not to std::vector<T> v{1, 2, 3}
, it's probably because direct-initialization is represented differently in GCC, which causes it to not be detected by this optimization.