Open rkjnsn opened 1 month ago
Needs reduced testcase that doesn't involve the full std::optional. (Looked briefly, but the issue seems to be deep inside the constructor for std::optional.)
Unfortunately, the issue doesn't reproduce with a simple optional analog, so it's something within the complexity of std::optional
's implementation that is triggering the bug.
class ConstEval {
public:
consteval ConstEval(const char* value) {
throw "Disallowed character!";
}
};
struct FakeOptionalBase {
ConstEval val;
template <class Anything = int> constexpr
FakeOptionalBase(const char (&arg)[12]) : val(arg) {}
};
struct FakeOptional : FakeOptionalBase {
using FakeOptionalBase::FakeOptionalBase;
};
void TakesOptional(FakeOptional maybe_value) {
}
int main(int argc, const char*argv[]) {
TakesOptional(FakeOptional("tasting 123"));
}
The key here seems to be that the inherited constructor isn't marked as an immediate function in the AST.
(CC @cor3ntin)
@llvm/issue-subscribers-clang-frontend
Author: Erik Jensen (rkjnsn)
To bring some relevant PR discussion to the bug,
In @cor3ntin's PR #112860, @efriedma-quic provides the following test case:
struct ConstEval {
consteval ConstEval(int) {}
};
struct SimpleCtor { constexpr SimpleCtor(int) {}};
struct TemplateCtor {
template <class Anything = int> constexpr
TemplateCtor (int arg) {}
};
struct ConstEvalMember1 : SimpleCtor {
int y = 10;
ConstEval x = y;
using SimpleCtor::SimpleCtor;
};
struct ConstEvalMember2 : TemplateCtor {
int y = 10;
ConstEval x = y;
using TemplateCtor::TemplateCtor;
};
void f() {
ConstEvalMember1 i1(0);
ConstEvalMember2 i2(0);
}
If the inherited derived class constructor copies the immediate-escalating property of the base class, this leads to the counter-intuitive behavior that i1
produces an error while i2
doesn't.
Intuitively, given that a compiler-generated default constructor is immediately-escalating, and an inheriting constructor in a derived class effectively acts as a compiler-generated default constructor except that it initializes the relevant base class with the inherited constructor, I would expect the generated derived-class constructor always to be immediate-escalating, and for any immediate field or base class initialization (including, but not limited to, the base class from which the constructor is inherited) to cause the compiler-generated constructor to escalate to being immediate. Thus, I would expect both i1
and i2
to compile.
That said, I was not able to find any language in the standard confirming or denying that that's how immediate escalation with inherited constructors should work. However, I'm far from a standards expert, so I very well might be missing something.
Consider the following code:
Assuming I'm reading things correctly, I would expect the
std::optional
constructor to be an immediate-escalating function (because it is "a function that results from the instantiation of a templated entity defined with the constexpr specifier"), and a contained call toConstEval
's constructor to be an immediate-escalating expression, causing thestd::optional
constructor to become an immediate function.Thus, I would expect lines 1, 2, 3, 5, and 6 all to immediately invoke the
std::optional
constructor to create a compile-time constantstd::optional
(indirectly invoking theConstEval
constructor), while 4 would immediately invoke theConstEval
constructor to create a constantConstEval
, which would then passed to the (not immediate, in this case)std::optional
constructor.Given that, I would further expect that changing "testing" to "tasting" on any of the 6 lines would result in a compiler failure (specifically, one informing me that
throw "Disallowed character!"
is not valid in a constant expression).However, with clang built from revision 3dbd929ea6af134650dd1d91baeb61a4fc1b0eb8, only lines 3, 4, and 6 fail to compile if "testing" is changed to "tasting". Lines 1, 2, and 5 unexpectedly continue to compile if "testing" in changed to "tasting", and instead cause an abort at runtime due to the uncaught exception. Thus, it appears that while clang properly considers the invocation of the explicitly-immediate
ConstEval
constructor to be a constant expression in 4, it erroneously does not consider the invocation of the escalated-to-immediatestd::optional
constructor to be a constant expression in 1, 2, and 5. Meanwhile, 3 and 6 invoke the constructor within an explicit constant expression, so those work as expected.A further note: the observed behavior of
ConstEval
's constructor executing (and crashing if "testing" is changed to "tasting") at runtime for 1, 2, and 5 appears only to happen if it's small enough to inline into the generatedstd::optional
specialization. If the constructor is more complicated and doesn't get inlined, lines 1, 2, and 5 will instead generate a linker error due to the symbolConstEval::ConstEval(char const*)
not being defined.