C++ Logo

std-proposals

Advanced search

Lifetime Extending constructors

From: connor horman <chorman64_at_[hidden]>
Date: Fri, 2 Jul 2021 11:15:03 -0400
Currently, in C++, it is possible to pass temporaries to constructors (that
accept a `const&` or rvalue reference type), however, those temporaries are
not lifetime extended, and thus are destroyed at the end of the full
expression (though there are exceptions for certain builtin types, such as
references and std::initializer_list, and for aggregate construction using
list initialization). This is correct for some types (such as owning
container types), but not for types like std::span or
std::reference_wrapper, which borrows it's parameter. As a result, I have a
lot of code like this:

const std::string_view strings[]{"foo"sv,"bar"sv,"baz"sv};
const std::span<const std::string_view> actual_strings_link{strings};

This is used in code where storing "pointers" to arbitrarily sized arrays
is desired (Example using types designed to replace stdlib types:
https://github.com/LightningCreations/lccc/blob/012438ad114526e1ad9fbe4075d718340c3a1d51/xlang/src/Targets/X86.cpp#L49
)
Though noting the (presumably deliberate) lack of an initializer_list
constructor for std::span, it would be nice if this could be rewritten as
something like this, to avoid the single-use static arrays:

const std::span<const std::string_view>
actual_strings_link{"foo"sv,"bar"sv,"baz"sv};

To support this, I propose the [[extend_temporary_lifetime]] attribute. The
attribute can be applied to a const reference, rvalue reference, or
std::initializer_list parameter of a non-copy, non-move constructor. If the
constructor is invoked with a temporary argument in the position of the
temporary, then the lifetime of that temporary is extended to match the
lifetime of the object under construction. If the object being constructed
is a prvalue that initializes another object, the lifetime of the
initialized object is used instead. These rules apply any place lifetime
extension applies: to top level objects, and data members under aggregate
initialization of top level objects. (Notably, a constructor reference
parameter without the attribute used to initialize a non-static data member
will not cause lifetime extension even if it invokes a constructor with
that attribute). Additionally, neither copy nor move construction will
further extend the lifetime of temporaries.

As a side-proposal, but not necessarily required,I also propose the
addition of a std::initializer_list constructor with this attribute, to
std::span. While std::span can be initialized from a value of type
std::initializer_list (due to the range overload), it cannot (
https://godbolt.org/z/osr7nh7EY) be list-initialized. While previously,
such a constructor would be a potential footgun, with this attribute, it
could be used safely and thus would be a nice QOL improvement.
The signature would be:
constexpr explicit(extent!=dynamic_extent)
span([[extend_temporary_lifetime]] std::initializer_list<T>)
and would only participate in overload resolution if const T(*)[] is
convertible to T(*)[].Other than the lifetime extension, the constructor
would act identically to the Range constructor, thus no well-defined code
currently constructing std::span with an initializer_list would be
affected.

Prior Art:
- As mentioned, C++ does perform lifetime extension of "constructor"
arguments in a few cases. When initializing an object of type
std::initializer_list<T> from a braced-init-list, a temporary of type const
T[N] is initialized with the same list, then lifetime extended to that
object (which the initializer_list then refers to). Variables (and statics)
of reference types also benefit from this behaviour. For user-defined
types, aggregate initialization, as mentioned, will perform lifetime
extension for reference and std::initializer_list members.
- In rust, constructors are simple functions, but due to it's explicit
lifetime system, temporary lifetime extension is rather simple to apply.
Consider the Following type which is effectively equivalent to
std::span<const T>:
pub struct ConstSpan<'a,T>{
    data: *const T,
    size: usize,
    lifetime: PhantomData<&'a [T]>
}

impl<'a,T> ConstSpan<'a,T>{
    pub const fn new(slice: &'a [T]) -> ConstSpan<'a,T>{
         Self{data: slice.as_ptr(),size: slice.len(), lifetime: PhantomData}
    }
    // Other methods, indexing/deref impls,
}

In Rust, you could easily construct a ConstSpan from an array using similar
syntax to C++ list-initialization: `ConstSpan::new(&[1,2,3,4]);`. Because
Rust can see the dependency of the return value of new on the parameter, it
knows that the lifetime of the array (which is a temporary) must be at
least as long as the lifetime of the value. While this proposal is nowhere
near as versatile (Rust can even track that dependency when moving/copying
ConstSpan), this proposal provides at least some of that benefit, without
turning C++ into Rust 2.0.

Drawbacks:
- The attribute proposed here would have notable behavioural change between
code that does not use the attribute and code that does. Thus,
implementations would be required to support the attribute (which does not
apply to any other standard attribute). A different syntax form may be
reasonable to indicate this, but an attribute was the first thing I thought
of

Breaking Changes:
- Adding this attribute should not be a breaking change as it does not
affect code that does not use the attribute. Applying the attribute to
functions, however, may be (as it changes the destruction timing of
temporaries it applies to).
- Adding the span method described above would likely not be a breaking
change in reasonable code. However, I do not believe there is a requirement
that std::span not be dangling (it effectively cannot be used while
dangling, but I believe it can be). Thus, this would affect the observable
behaviour of any code that constructs a span (either as a local variable or
static) from a temporary initializer_list, then proceeds to ignore that
span. I do not believe this is affects a significant amount of code.

Additional use in the standard Library:
As mentioned adding this attribute to parameters may cause breaking
changes. However, by the same reasoning, that the number of cases that
would be broken and that don't have undefined behaviour is likely few, I
propose to add the attribute to all of the following constructors in the
Standard Library:
- The Range and Array constructors of std::span
- The upcoming Range constructor of std::basic_string_view.

Specification:

The attribute [[extend_temporary_lifetime]] may be applied to a reference
or std::initializer_list parameter of a constructor, other than a copy or
move constructor. If multiple declarations or definitions of the same
constructor within the same translation unit, then each parameter that has
the attribute in at least one declaration or definition is treated as
though it had the attribute in all subsequent declarations. If a
translation unit declares or defines a constructor with a parameter that
has the attribute, and a different translation unit declares or defines a
constructor that does not apply the attribute to that parameter, the
program is ill-formed, no diagnostic required.

If a complete object, or a subobject initialized during aggregate
initialization of a complete object, with automatic, static, thread, or
lifetime-extended temporary storage duration, is initialized by calling a
constructor other than a copy or a move constructor or a prvalue
initialized in such a way, then if initialization of that object causes a
temporary object to be materialized and bound to a parameter declared with
this attribute, then the temporary object materialized is lifetime-extended
to match the lifetime of the object being initialized.

Received on 2021-07-02 10:15:24