Extending `[[clang::lifetimebound]]` to take a condition

Our Optional has a value_or member function that is implemented as “just do what ?: does”:

template <class T>
struct Optional {
  template <class U>
  decltype(auto) value_or(U&& rhs) const& {
    return *this ? **this : rhs;
  }
};

Except with 4 overloads to handle compilers that don’t support explicit object parameters yet.

This can produce a dangling reference, which I want to be diligent and warn on with [[clang::lifetimebound]]. The problem is, this is not a case where I can unconditionally slap that attribute (unlike operator*, operator->, value(), etc.). There are two places where I can potentially put [[clang::lifetimebound]], in different situations.

Let R be the return type. What we want to do is:

  • Put [[clang::lifetimebound]] on rhs when R is a reference type.
  • Put [[clang::lifetimebound]] on the object parameter (i.e. trailing behind the function) when R is a reference type and T is not a reference type.

But I can’t do that with [[lifetimebound]], it doesn’t take a condition. Which means, like pre-C++20 explicit, the only way I can achieve this is by adding 2 more overloads of value_or. Which probably makes the code take longer to compile, since now I’m both declaring more function templates and also have more overload resolution candidates. But it definitely makes hte code much more difficult to understand (the above criteria is already pretty subtle, but now imagine if that criteria is just implicit based on how it’s splayed out across all our overloads).

With [[lifetimebound(condition)]] this is much clearer. Also this isn’t our first or only such desired use of conditional-lifetimebound, we’ve run into it in several other places already too.

1 Like

Thank you for the feedback and suggestion! Can you share a code example of how you’d like your code to look with this feature? I can think of a few different designs for this based on what you’ve described, so I just want to be sure we’re on the same page.

CC @usx95 @NeKon69 for opinions as they’re the ones most heavily involved in the analysis in this area.

Can you share a code example of how you’d like your code to look with this feature?

The most direct thing is just to allow a condition, something like this:

template <class U, class R = decltype(...)>
R value_or(U&& rhs [[clang::lifetimebound(is_reference_v<R>)]]) const& [[clang::lifetimebound(is_reference_v<R> and not is_reference_v<T>)]] {
  // ...
}

Is this getting pretty busy and hard to read? Yes it is! But I think still appreciably easier to understand than status quo.

Needless to say, I’m also open to anything better than that. Supporting a condition just seems like a small extension.

Thanks! The “put [[clang::lifetimebound]] on” phrasing made me wonder if you were thinking of something like:

template <class U, class R = decltype(...)>
R value_or(U&& rhs) const& [[clang::lifetimebound(*this ? **this : rhs)]] {
  // ...
}

which is … significant magic.

I think what you’re proposing is chatty, but I also think the situations in which it’s needed are limited enough that the chattiness is fine. So to me, I think the design is reasonable to consider. That said, I have no idea if the condition poses other implementation burdens for the analysis.

So I may have a number of concerns with this, but I’ll start with the fact that [[clang::lifetimebound]] is a C++ attribute rather than a keyword like noexcept. So when generating the AST, clang will evaluate any argument of noexcept() and produce the appropriate AST nodes. So for example, if a function template ends up generating a number of distinct function instances, then presumably you could query each and any of the AST nodes corresponding to each of the function instances as to whether it is noexcept or not. (I don’t have any experience doing so, I’m just presuming.)

Whereas I assume that in most cases C++ attributes aren’t evaluated in the generation of the AST. Most C++ attributes are just attached as an attribute property of any corresponding AST nodes, right? So if [[clang::lifetimebound]] is going to become (optionally) conditional based on an expression argument, who’s going to evaluate that expression? I mean, are we thinking the static analysis code, or the AST generation code? Unless we need the conditional expression to support other static analysis elements, I wouldn’t think the static analysis code would be the most well-equiped to do so. So this would end up being more than just a static analysis concern.

Also, would we be setting a precedent for conditional attributes? If so, presumably we’d want to make it a general mechanism that would be available for other attributes as well. I mean, even in the given example I’m thinking you might also want a conditional [[nodiscard]] attribute depending on whether the return type is a reference type or not.

But rather than bothering the clang developers for such specific mechanisms, it would be nice if the programmer had access to some kind of, oh I don’t know, “metaprogramming” facility for doing things like this… Actually I don’t know enough about C++26 metaprogramming to know if it can conditionally disable attributes. I wonder if someone in this thread would be qualified to answer that? And maybe give an example. :slight_smile:

There are many potential extensions we could make for the existing lifetime annotations to make them more expressive. One of the questions is, how far are we willing to push this? At what point is it a better decision to fully embrace named lifetimes that should be able to handle most (all) scenarios, and potentially more?

That being said, named lifetimes are particularly challenging as we probably would represent them as type sugar that is often dropped in the compiler and making sure we can resugar or preserve the sugar is a lot of work.

See [RFC] Lifetime annotations for C++

That’s not entirely accurate; it depends on the definition of the attribute in the compiler and what the semantic handler does for the attribute. Some attributes retain expression argument nodes, some attributes evaluate the argument as a constant expression and only retain the resulting value, etc.

I would imagine the design for this being that the attribute accepts an optional ExprArgument, and if that’s nonnull in the semantic attribute, it’s evaluated wherever the information is needed (static analyzer, CFG, etc). This is not fundamentally different to other attributes like enable_if and diagnose_if, for example: Compiler Explorer

This is proposing to add an expression argument to an attribute. We have plenty of instances of that already.

That depends on how lifetime annotations handle the case where some of the types involved actually don’t even have reference semantics. I couldn’t find any examples of that in the RFC, so I can’t tell. Maybe!

Conceptually, what we want is as simple as (I might have some of the $as in the wrong place, sorry. Hopefully it’s clear what I mean):

template <class T>
struct Optional {
  template <class U>
  decltype(auto) $a value_or(U&& $a rhs) const& $a {
    return *this ? **this : rhs;
  }
};

What happens if we do something like:

auto opt_val() -> Optional<int>;
auto opt_ref() -> Optional<int&>;

auto test() -> void {
  auto&& v1 = opt_val().value_or(42L);

  int def = 42;
  auto&& v2 = opt_val().value_or(def);
  auto&& v3 = opt_ref().value_or(def);
}

For v1, value_or just returns a long, so this is totally fine — there’s no dangling or anything. Or v2, value_or returns an int const& that might refer into the Optional temporary, so this is bad. This could dangle. For v3, there’s similarly no dangling because v3 outlives def and the return doesn’t have to be bound to the Optional<int&> object.

This is expressible via [[lifetimebound(bool)]]. Is it expressible with the lifetime annotation work?

Ah ok, thanks for the clarification. I guess it makes sense, expose the facilities for evaluating the expression argument to whoever needs it rather than preemptively evaluating expressions that may not even be used. (Btw I’m not immediately finding documentation for ExprArgument, is it available (for 3rd party users of the AST)?) Though I’m still curious how verbose a metaprogramming solution would be in comparison.

So @Xazax-hun expressed my other concern. You want to effectively disable the [[clang::lifetimebound]] annotation for the case where the return type is not a reference. But of course the return type could itself contain pointers or references, and a full solution still needs verify the safety of those contained pointer/references, so it still needs annotations to indicate where those contained references came from.

The issue is that from a lifetime analysis point of view, both cases (where the return value is and is not a reference) need annotation, but those annotations may look significantly different because, lifetime-wise, you’re actually doing significantly different things.

I don’t recall how it works in the linked “Lifetime annotations for C++” RFC, but with the lifetime annotation system I work on, you can associate a “lifetime label alias” to the set of (contained/owned) lifetimes associated with the type of a specified template parameter.

Here’s a lifetime annotated example of a pseudo Optional template implemented with overloads of value_or() for when the rhs parameter is an rvalue reference and an lvalue reference. Notice that for the lvalue overload (that returns a reference), on line 24 the rhs parameter is annotated with LTP(a) which just associates the parameter (which is of reference type) with the lifetime label a. (Note that a can be used to refer to the enitire heirarchy of lifetimes associated with the reference parameter.)

But for the rvalue overload that always returns a non-reference, on line 17 the rhs parameter is annotated with LTP(_[a$]) which associates the “lifetime set alias” a$ to the set of lifetimes owned by the target object of the reference parameter. The lifetime of the reference target itself is excluded, as it will play no role in the return value.

So on line 24 in the lvalue overload, we are always returning a raw reference which always has only one target object (but any number of sub-target objects in a heirarchy). But on line 17 where the return type is non-raw-reference type T, the return value may be of some type which has multple target objects, and thus multiple “root” lifetimes, and those lifetimes are not associated directly with the reference parameter, but rather the target object of the reference parameter. So lifetime-wise, it’s more complicated to express what we’re doing on line 17 than the relatively simple thing we’re doing on line 24.

Currently this lifetime annotation system does not support conditional lifetime annotations, but even if it did, I can’t imagine it would be any prettier or clearer than having separate implementations for reference and non-reference return types. And as far as I know, Rust does not support conditional lifetime annotations either. And note that currently [[clang::lifetimebound]] is supported and just interpreted as a simple lifetime label.

edit:
I’ll just add that while this comment may demonstrate why it might be better to have separate overloads when using “full” lifetime annotations for this example, I wouldn’t consider it a conclusive argument against the original proposition.

Also, while our safety standards remain low enough that we can’t accept false positives, we also can’t have implicit default lifetime annotations like Rust does. So the places where lifetime analysis is desired will have to be explicitly lifetime annotated (so that any false positives would be the programmer’s fault). So that might argue in favor of whatever ergonomic features necessary to reduce the burden, which may include adding optional conditionality to the lifetime annotations (“legacy” and otherwise).

But if ever our standards rise to require full lifetime safety, and the acceptance of some false positives in return, then we can introduce default implicit lifetime annotations. And in C++ I feel it might be appropriate for those defaults to be significantly more aggressive than, for example, Rust’s. And as we see even with Rust’s conservative default implicit annotations, the prevalence of lifetime annotations is significantly reduced.

edit2:
Oops, I was incorrectly assuming the return type of value_or() would always be a reference if the element type was a reference type. Corrections attempted.

Our documentation for this lives at:
https://clang.llvm.org/docs/InternalsManual.html#arguments
which is pretty lacking. :smiley: But tl;dr is: Clang’s Attr.td describes the arguments attributes are allowed to take, and tablegen translates that into various interfaces to automate some parts of attribute handling in a generic way. So we have ExprArgument, IntArgument, StringArgument, etc as ways to represent that in tablegen.

Aah, I see! Unfortunately, the only option I could think of is to have two different functions guarded by some SFINAE/requires on is_reference_v<R> or similar and the two version have different annotations.

It seems like this is only really useful in templated contexts. Instead of taking an arbitrary expression, could it be possible to make the attribute a template instead? I.e. something like:

template <class T>
struct Optional {
  template <class U>
  decltype(auto) value_or(U&& rhs [[clang::lifetimebound<decltype(rhs)>]] const& [[clang::lifetimebound<decltype(*this ? **this : rhs)>]] {
    return *this ? **this : rhs;
  }
};

If the type passed to the attribute is not one where lifetimebound would apply, it’d be ignored.

Another option would be to keep the current syntax, but simply not emit an error when lifetimebound fails to apply to the types in a template specialisation, and silently drop it instead.