https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127513
Bug ID: 127513
Summary: [15/16/17 Regression] array of class with
initializer_list constructor fails overload resolution
depending on optimization level and other conditions
Product: gcc
Version: 17.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: svetli97 at gmail dot com
Target Milestone: ---
Found on Windows 11 with GCC 15.2 (MinGW), later tested on godbolt.org for
different compiler versions (13.1 up to trunk).
The compiler fails to recognise std::initializer_list (only) constructor as a
valid default constructor when a class is the element type of an array under
specific conditions:
- 13.1+ (or older?) - std::array of class elements fails to default initialise
because it can't find the valid default constructor of the class under some
questionable conditions;
- Regression 14.4 -> 15.1 - optimisation level introduced the same bug for
dependant member initialisation;
- Regression 16.2 -> 17.0 (trunk) - optimisation level now also affects C-style
arrays the exact same way the bug affects std::array.
The C++ standard defines overload resolution as depending only on what function
declarations are available. It must *not* depend on optimisation level, whether
the function is defined as constexpr, how the initialisation of the class'
members might depend on one another when the constructor is called, or whether
the function would cause UB if called.
The original bug I found was the regression from 14.4 to 15.1 but while trying
to recreate it in isolation, I stumbled upon other conditions that lead to the
exact same incorrect compilation error message. So I'm putting them all in the
same report. The error message is (example with the first class name):
error: no matching function for call to 'S::S()'
note: candidate: 'S::S(std::initializer_list<int>)'
note: candidate expects 1 argument, 0 provided
(code at the end)
Expected behaviour: S::S(std::initializer_list<int>) should be recognised as a
default constructor in all the examples below, not just an arbitrary subset of
them.
The preconditions for the original bug (the regression) I found are the most
specific (and rule-breaking):
- std::array of a class (also C-style array with GCC 17.0);
- the class must only have std::initializer_list constructor;
- the constructor must be constexpr (to avoid triggering the bug in another
way);
- one class field must depend on a previous one in its initializer;
- optimisation level must be -O1, -O2, -O3, or -Os (only -O0 avoids the bug).
See "std::array<C0_3, 5> shiny_fail {};" and "C0_3 shiny_trunk_fail[5] {};".
And just in case I missed some preconditions, the planets currently in
retrograde are: Saturn, Uranus, Neptune, Pluto, and Chiron.
I suspect the compiler is dropping the constructor because it assumes it is
invalid due to the dependance between the two fields (possibly optimised in a
weird way) even though there is no actual problem there. And then it falls into
the same erroneous state as with all the following error examples where the
compiler doesn't like how a constructor would behave if called so it completely
drops it from the overload resolution.
Now the conditions that cause the bug in all GCC versions I've tested (13.1 -
17.0):
Class S is the absolute minimal example:
struct S {S(std::initializer_list<int> li){}};
And yet an array of this class doesn't compile:
std::array<S, 5> fail_1 {};
How this one fails is completely beyond me. If you make the constructor
constexpr (see class C), that one never fails. constexpr constructor is not a
requirement to construct a simple local variable.
My best guess is the compiler tries to enforce constexpr rules when noone
asked. And when they fail, in throws away the constructor entirely.
Because of this bug, all my other examples use constexpr constructors.
Classes C1 and C0_1 (arrays fail_2 and fail_3) likely fail because the compiler
sees that their default constructors would cause UB (nullptr dereference) and
then incorrectly decides it can remove them from overload resolution. There is
no rule that disqualifies functions because they would cause UB. But try all
the cases in isolation if you are worried that the UB could affect the other
ones.
Note that if you make ok_9 and fail_2 constexpr, ok_9 will correctly give the
nullptr dereference compiler error, but fail_2 will still give the wrong "no
matching function" error.
I randomly stumbled upon this case while stripping away code until I figured
out the exact conditions for the regression as any tiny change I made to C0_3
(shiny_fail) would cause it to either fail or succeed on all compiler versions.
But I needed to find was causing my large project that worked before to fail
when upgrading GCC. And I initially incorrectly assumed it was some previously
undetected UB I caused.
The test cases at the end show the exact same fail cases as all of the above
but with the regression that extended the bug to C-style arrays as well.
All the conditions indicate that overload resolution checks and drops
constructors based on rules that are not in the standard.
For reference, clang correctly compiled the code with all versions I tried. And
gave the correct errors when compiling the UB cases with constexpr variables.
To whoever is maintaining the millions of "if" statements that handle all these
edge cases of this abomination of a programming language, I pray you have a
fruitful week.
Best wishes,
Svetlin
Source code I used (I recommend compiling it in parallel for 14.4, 15.1, and
17.0 to see the regressions and the old but still remaining bugs):
#include<array>
struct S {S(std::initializer_list<int> li){}};
struct C {constexpr C(std::initializer_list<int> li){}};
struct C0 {
std::array<int, 2> vals;
constexpr C0(std::initializer_list<int> li):
// literals are ok
vals {1, 2}
{}
};
struct C1 {
std::array<int, 2> vals;
constexpr C1(std::initializer_list<int> li):
// unconditional dependance on li fails
vals {li.begin()[0], li.begin()[1]}
{}
};
struct C2 {
std::array<int, 2> vals;
constexpr C2(std::initializer_list<int> li):
// conditional dependance on li is ok
vals {li.size() ? li.begin()[0] : 0,
li.size() > 1 ? li.begin()[1] : 0}
{}
};
struct C0_0 {
std::array<int, 2> vals;
int magic;
constexpr C0_0(std::initializer_list<int> li):
vals {1, 2},
// literal is ok
magic {42}
{}
};
struct C0_1 {
std::array<int, 2> vals;
int magic;
constexpr C0_1(std::initializer_list<int> li):
vals {1, 2},
// unconditional dependance on li fails
magic {li.begin()[0]}
{}
};
struct C0_2 {
std::array<int, 2> vals;
int magic;
constexpr C0_2(std::initializer_list<int> li):
vals {1, 2},
// conditional dependance on li is ok
magic {li.size() ? li.begin()[0] : 0}
{}
};
struct C0_3 {
std::array<int, 2> vals;
int magic;
constexpr C0_3(std::initializer_list<int> li):
vals {1, 2},
// dependance on vals fails on GCC 15.1+
// but it's ok on all previous tested versions
magic {vals[0]}
{}
};
int main() {
// fails on all GCC versions I tested (13.1+)
std::array<S, 5> fail_1 {};
std::array<C, 5> ok_1 {};
std::array<C0, 5> ok_2 {};
std::array<C1, 5> fail_2 {}; // fails on all
std::array<C2, 5> ok_3 {};
std::array<C0_0, 5> ok_4 {};
std::array<C0_1, 5> fail_3 {}; // fails on all
std::array<C0_2, 5> ok_5 {};
// fails on GCC 15.1+ with optimisations (-O1, -O2, -O3, -Os)
//ok on GCC 14.4 and earlier or with -O0 on any version
std::array<C0_3, 5> shiny_fail {};
// directly default-initializing works fine
S ok_6 {};
C ok_7 {};
C0 ok_8 {};
C1 ok_9 {};
C2 ok_10 {};
C0_0 ok_11 {};
C0_1 ok_12 {};
C0_2 ok_13 {};
C0_3 ok_14 {};
// C-style arrays are ok up to GCC 16.2 but
// fail exactly like std::array on 17.0 (trunk)
S trunk_fail_1[5] {}; // fails on trunk, ok on 16.2
C ok_15[5] {};
C0 ok_16[5] {};
C1 trunk_fail_2[5] {}; // fails on trunk, ok on 16.2
C2 ok_17[5] {};
C0_0 ok_18[5] {};
C0_1 trunk_fail_3[5] {}; // fails on trunk, ok on 16.2
C0_2 ok_19[5] {};
// fails on trunk with optimisations (-O1, -O2, -O3, -Os)
// ok on 16.2 and earlier or with -O0 on any version
C0_3 shiny_trunk_fail[5] {};
return 0;
}