3436. std::construct_at should support arrays

Section: 26.11.8 [specialized.construct] Status: Ready Submitter: Jonathan Wakely Opened: 2020-04-29 Last modified: 2024-06-24

Priority: 2

View other active issues in [specialized.construct].

View all other issues in [specialized.construct].

View all issues with Ready status.

Discussion:

std::construct_at is ill-formed for array types, because the type of the new-expression is T not T* so it cannot be converted to the return type.

In C++17 allocator_traits::construct did work for arrays, because it returns void so there is no ill-formed conversion. On the other hand, in C++17 allocator_traits::destroy didn't work for arrays, because p->~T() isn't valid.

In C++20 allocator_traits::destroy does work, because std::destroy_at treats arrays specially, but allocator_traits::construct no longer works because it uses std::construct_at.

It seems unnecessary and/or confusing to remove support for arrays in construct when we're adding it in destroy.

I suggest that std::construct_at should also handle arrays. It might be reasonable to restrict that support to the case where sizeof...(Args) == 0, if supporting parenthesized aggregate-initialization is not desirable in std::construct_at.

[2020-05-09; Reflector prioritization]

Set priority to 2 after reflector discussions.

[2021-01-16; Zhihao Yuan provides wording]

Previous resolution [SUPERSEDED]:

This wording is relative to N4878.

  1. Modify 26.11.8 [specialized.construct] as indicated:

    template<class T, class... Args>
      constexpr T* construct_at(T* location, Args&&... args);
    
    namespace ranges {
      template<class T, class... Args>
        constexpr T* construct_at(T* location, Args&&... args);
    }
    

    -1- Constraints: The expression ::new (declval<void*>()) T(declval<Args>()...) is well-formed when treated as an unevaluated operand.

    -2- Effects: Equivalent to:

    returnauto ptr = ::new (voidify(*location)) T(std::forward<Args>(args)...);
    if constexpr (is_array_v<T>)
      return launder(location);
    else
      return ptr;
    

[2021-12-07; Zhihao Yuan comments and provides improved wording]

The previous PR allows constructing arbitrary number of elements when T is an array of unknown bound:

extern int a[];
std::construct_at(&a, 0, 1, 2);

and leads to a UB.

Previous resolution [SUPERSEDED]:

This wording is relative to N4901.

  1. Modify 26.11.8 [specialized.construct] as indicated:

    template<class T, class... Args>
      constexpr T* construct_at(T* location, Args&&... args);
    
    namespace ranges {
      template<class T, class... Args>
        constexpr T* construct_at(T* location, Args&&... args);
    }
    

    -1- Constraints: The expression ::new (declval<void*>()) T(declval<Args>()...) is well-formed when treated as an unevaluated operand (7.2.3 [expr.context]) and is_unbounded_array_v<T> is false.

    -2- Effects: Equivalent to:

    returnauto ptr = ::new (voidify(*location)) T(std::forward<Args>(args)...);
    if constexpr (is_array_v<T>)
      return launder(location);
    else
      return ptr;
    

[2024-03-18; Jonathan provides new wording]

During Core review in Varna, Hubert suggested creating T[1] for the array case.

Previous resolution [SUPERSEDED]:

This wording is relative to N4971.

  1. Modify 26.11.8 [specialized.construct] as indicated:

    template<class T, class... Args>
      constexpr T* construct_at(T* location, Args&&... args);
    
    namespace ranges {
      template<class T, class... Args>
        constexpr T* construct_at(T* location, Args&&... args);
    }
    

    -1- Constraints: is_unbounded_array_v<T> is false. The expression ::new (declval<void*>()) T(declval<Args>()...) is well-formed when treated as an unevaluated operand (7.2.3 [expr.context]).

    -2- Effects: Equivalent to:

    if constexpr (is_array_v<T>)
      return ::new (voidify(*location)) T[1]{{std::forward<Args>(args)...}};
    else
      return ::new (voidify(*location)) T(std::forward<Args>(args)...);
    

[St. Louis 2024-06-24; Jonathan provides improved wording]

Why not support unbounded arrays, deducing the bound from sizeof...(Args)?
JW: There's no motivation to support that here in construct_at. It isn't possible to create unbounded arrays via allocators, nor via any of the uninitialized_xxx algorithms. Extending construct_at that way seems like a design change, not restoring support for something that used to work with allocators and then got broken in C++20.

Tim observed that the proposed resolution is ill-formed if T has an explicit default constructor. Value-initialization would work for that case, and there seems to be little motivation for supplying arguments to initialize the array. In C++17 the allocator_traits::construct case only supported value-initialization.

[St. Louis 2024-06-24; move to Ready.]

Proposed resolution:

This wording is relative to N4981.

  1. Modify 26.11.8 [specialized.construct] as indicated:

    template<class T, class... Args>
      constexpr T* construct_at(T* location, Args&&... args);
    
    namespace ranges {
      template<class T, class... Args>
        constexpr T* construct_at(T* location, Args&&... args);
    }
    

    -1- Constraints: is_unbounded_array_v<T> is false. The expression ::new (declval<void*>()) T(declval<Args>()...) is well-formed when treated as an unevaluated operand (7.2.3 [expr.context]).

    -?- Mandates: If is_array_v<T> is true, sizeof...(Args) is zero.

    -2- Effects: Equivalent to:

    if constexpr (is_array_v<T>)
      return ::new (voidify(*location)) T[1]();
    else
      return ::new (voidify(*location)) T(std::forward<Args>(args)...);