Currently something like this requires a newtype that implements Display, which is far from a one-liner.
It could also be a way to have format!'s formatting string vary dynamically, while still being a single expression, without the inconvenience of rewriting it to use temporary String and write!.
format!("There {}!", if n == 1 {
format_args!("is one apple")
} else {
format_args!("are {n} apples")
});
But unfortunately format_args! currently borrows its arguments only for its tiny own scope, which makes it unusable for anything involving any larger scope.
It seems hard to achieve this for format_args! (or something like format_args!), if the goal is to have it create the same type everywhere; unless you’d want to create heap allocations. The whole structure inside of format_args! is saved in temporary variables, and then references to these variables are saved in the Arguments<'_> structs.
For comparison, its simple to create a macro that returns an owned impl Display which is then allowed to live longer, containing only borrows of its arguments, and not of its own internally used temporary variables (e.g. by expanding the macro to exactly the kind of display_with call I’ve shown above – but as soon as you’d try to adapt this to create a uniform &dyn Display type, you’d need to have the macro expression itself create a temporary, and then borrow from that temporary, which results in the same lifetime situation as the original format_args! does – or alternatively one could create a Box<dyn Display>.
One third option that comes to mind is that one could be allowed to just pass a custom variable to the macro, so that that variable can then own the value (e.g. the impl Display value), instead of having it be held by a temporary. I’ll see if I can write a code example for that…
I had to think for a while to get why that works when an if-else-expression doesn't, but it seems to boil down to how the latter is not eligible for temporary lifetime extension. That pattern can even be extended using Option::or (and similar methods):
format!("There {}!",
(n == 0).then_some(format_args!("are no ({n}) apples"))
.or((n == 1).then_some(format_args!("is just one ({n}) apple")))
.or((n == 2).then_some(format_args!("are two ({n}) apples")))
.unwrap_or(format_args!("are {n} apples"))
)
Another option is indexing into an array of choices:
pub fn array(n: usize) -> String {
format!("There {}!",
[
format_args!("are no ({n}) apples"),
format_args!("is just one ({n}) apple"),
format_args!("are two ({n}) apples"),
format_args!("are {n} apples"),
][n.min(3)]
)
}
Those still left me wondering if there's any way to get the same effect without evaluating format_args for the cases that aren't taken. It seemed impossible for a while, since almost all forms of conditional evaluation create drop scopes. However, a combination of label-break-value and None getting constant promoted did lead to this solution:
Not really any "extension". It's more like the normal rules of when temporaries are dropped, i. e. the rule you would typically explain to beginners as "at the end of the containing statement", but which actually also includes things like match arms, and blocks of if expressions, the condition of an if but not an if else expression, etc..
Lovely! I've wondered the same but hadn't found a solution yet.
Firstly, I believe the issue is the same as with this: (i.e. not specific to format_args)
//error[E0716]: temporary value dropped while borrowed
consume(
//----- borrow later used by call
if cond {
&then_block(4)
// ^^^^^^^^^^^^^ creates a temporary which is freed while still in use
} else {
// - temporary value is freed at the end of this statement
&else_block(7)
}
);
I agree that ought to compile, but I have no idea if it could be changed.
Labeled breaks and tuple evaluation do enable a workaround:
I’m not sure if that’t the official reason, but I do feel like the rules that limit temporary scopes for almost all cases of conditional evaluation serves the purpose of eliminating drop flags for temporaries. Which is typically a good thing.
Of course, your example demonstrates that for the case of labeled blocks, that is no longer fully the case. (Labeled blocks might be the only construct though.) So using your if_then_else macro, when temporaries are eventually dropped e.g. at the end of the enclosing statement, the question whether which ones need their destructors to be run is determined by the drop flags at run-time.
Interestingly, for the case of Arguments<'_>, there should be no drop glue involved in the first place. I wonder how sane it would be to consider changing Rust to extend the lifetime of temporaries based on whether or not their types have drop glue. On first thought, this would not even affect program behavior, or would it?
And drop glue and drop implementations can already influence whether or not a program compiles e.g. in the drop check, or for destructuring … though admittedly, this change may make drop glue more relevant. If we’re worried about new Drop implementations becoming a breaking change in cases where they were a backwards-compatible change previously, we could instead further limit the extended lifetime of temporaries in conditional expressions[1] to only types implementing Copy. ( fmt::Arguments<'_>does implement Copy!) For Copy, removing a Copy impl is already a breaking change; and adding one would only make more use-cases compile and when exactly temporaries are dropped shouldn’t affect program behavior anyways. Edit: Side-note: I just realized that adding a Copy implementation can break code, as it affects closure captures, and can e.g. turn a 'static closure into a non-'static one.
(Anyone feel free to point out any oversights. Or ask questions, in case my thoughts were unclear.)
I haven’t checked the reference, but from testing it, it seems like they aren’t eligible. For helping the case of format_args it wouldn’t matter anyways.
Because format_args! doesn’t expand to something like an expression of the form &function_call(…) which would be eligible for lifetime extension, but rather to something akin to outer_function_call(&inner_function_call(…)), which isn’t eligible for lifetime elision in any context, anyways. I.e. something like
fn f() {
let n = 1;
let a = format_args!("hi {n}");
println!("{a}");
}
FYI, since format_args no longer is an ordinary macro, the best way I’ve found to inspect its expansion is by looking at HIR. E.g. if you show the HIR for code like this
fn f() {
let s = "hi";
format_args!("Hello {s:?}");
}
in a playground, you see the desugaring into HIR shown as follows
fn f() {
let s = "hi";
<#[lang = "format_arguments"]>::new_v1(&["Hello "],
&[<#[lang = "format_argument"]>::new_debug(&s)]);
}
which in proper Rust code would be something like
fn f() {
let s = "hi";
core::fmt::Arguments::new_v1(
&["Hello "],
&[core::fmt::rt::Argument::new_debug(&s)],
);
}
Yeah, the issue is probably just the fact it’s using functions to construct, I have my macro expand to actual struct constructors for specifically this reason.