-
Notifications
You must be signed in to change notification settings - Fork 15.5k
Description
Given the following definitions:
#include <optional>
struct s
{
const std::optional<int>& x = std::nullopt;
};
struct t
{
const s& v;
};and the following usage:
#include <iostream>
void f(const s& args)
{
if (args.x)
{
std::cout << *args.x << std::endl;
}
else
{
std::cout << "N/A" << std::endl;
}
}
void g(const t& args)
{
f(args.v);
}we're seeing a stack-use-after-return ASAN error for the following case:
int main()
{
const t args{.v = {}};
g(args);
}Looking at the LLVM IR, I'm seeing:
define dso_local void @test4()() local_unnamed_addr {
entry:
%ref.tmp = alloca %struct.s, align 8
%ref.tmp1 = alloca %"class.std::optional", align 4
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %ref.tmp) #6
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %ref.tmp1) #6
store ptr %ref.tmp1, ptr %ref.tmp, align 8
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %ref.tmp1) #6
call void @f(s const&)(ptr noundef nonnull align 8 dereferenceable(8) %ref.tmp)
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %ref.tmp) #6
ret void
}
where the lifetime of ref.tmp1 does not extend.
With clang 19, this produced warning lifetime extension of temporary created by aggregate initialization using a default member initializer is not yet supported; lifetime of temporary will end at the end of the full-expression.
In contrast, the following works fine:
int main()
{
const s args{};
f(args);
}and lifetime of the default-initialized optional gets extended:
define dso_local void @test2()() local_unnamed_addr {
entry:
%args = alloca %struct.s, align 8
%ref.tmp = alloca %"class.std::optional", align 4
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %args) #6
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %ref.tmp) #6
%_M_engaged.i.i.i.i = getelementptr inbounds i8, ptr %ref.tmp, i64 4
store i8 0, ptr %_M_engaged.i.i.i.i, align 4
store ptr %ref.tmp, ptr %args, align 8
call void @f(s const&)(ptr noundef nonnull align 8 dereferenceable(8) %args)
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %ref.tmp) #6
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %args) #6
ret void
}
With GCC, lifetime seems to get extended.
I've shared a repro in godbolt at https://godbolt.org/z/PPPG6j53W. This has a custom my_opt<T> with a destructor that prints, to further illustrate the difference between GCC and clang. I.e.:
const t args{.v = {}};
g(args);prints
N/A
~my_opt
on GCC, i.e. destruction of the std::nullopt instance happens after the call to g, but prints
~my_opt
N/A
on clang.