admin管理员组

文章数量:1328037

Question- see Compiler Explorer

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?

Further investigation

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?

EDIT

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?

EDIT2

After the answer by cpplearner I ran a test which initializes an std::vector with string literals many times, using the v{} and v = {} syntaxes.

On my i9-10885H:

GCC 14.2

v{}   0.820 seconds
v={}  0.444 seconds
v={} took 54.1% as long as v{}

Clang 18.1

v{}   0.819 seconds
v={}  0.842 seconds
v={} took 102.8% as long as v{}

So it does appear this is a string literal optimization in GCC, which only works for the v = {} syntax.

Question- see Compiler Explorer

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?

Further investigation

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?

EDIT

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?

EDIT2

After the answer by cpplearner I ran a test which initializes an std::vector with string literals many times, using the v{} and v = {} syntaxes.

On my i9-10885H:

GCC 14.2

v{}   0.820 seconds
v={}  0.444 seconds
v={} took 54.1% as long as v{}

Clang 18.1

v{}   0.819 seconds
v={}  0.842 seconds
v={} took 102.8% as long as v{}

So it does appear this is a string literal optimization in GCC, which only works for the v = {} syntax.

Share Improve this question edited Jan 24 at 22:13 Josh Shields asked Jan 19 at 6:55 Josh ShieldsJosh Shields 1035 bronze badges 3
  • 5 To be clear, how are you determining which constructor got called? By looking at the disassembly? The init_list ctor is likely calling the const int *, const int * one internally. – HolyBlackCat Commented Jan 19 at 7:19
  • This is a very interesting question, but it's probably just some weird GCC optimization, where it knows the resulting code will be the same regardless. – HolyBlackCat Commented Jan 19 at 11:32
  • Your results may also vary depending on which C++ standard you are using. Because (iirc), the behavior was tweaked a little bit along the way. But I may be mistaken, and perhaps those tweaks are not germane to your example. – Eljay Commented Jan 19 at 12:10
Add a comment  | 

1 Answer 1

Reset to default 15

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.

本文标签: