Skip to content

Issue with lifetime extension of temporaries in default member initializers #171575

@bartdesmet

Description

@bartdesmet

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    clang:frontendLanguage frontend issues, e.g. anything involving "Sema"miscompilation

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions