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.
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.
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.
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):
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.
Our documentation for this lives at: https://clang.llvm.org/docs/InternalsManual.html#arguments
which is pretty lacking. 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:
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.
If v3 doesn’t outlive def and value_or returns a reference, then the result can dangle.
As an additional note, the GCC attribute [[gnu::no_dangling]] (which is in a similar role as [[clang::lifetimebound]]) can take a boolean condition in the way I’m requesting here, and we make use of that.
But, if v3 ends up being a reference to def from opt_ref().value_or(def), and v3 outlives def, would that not be dangling? Since v3 would be a reference to def, but def would eventually no longer exist if v3 outlives it.
I think this addition would be really neat as it will eliminate quite a number of false positives introduced with annotating templated functions. Since it was already brought up here, I’d also throw in my 5 cents – I think if we decide to add this, the most important part is where the condition is evaluated, and for this we have to decide whether the condition can accept something that only the analysis knows about. If we don’t need that, maybe we can get away with just AST-level evaluation (evaluate it at instantiation, like enable_if in the return type) because then this just becomes don’t-attach/attach the attribute per specialization, which means that we will probably have to make close to 0 changes in the analysis for this to work.