3149. DefaultConstructible should require default initialization

Section: 18.4.12 [concept.default.init] Status: C++20 Submitter: Casey Carter Opened: 2018-08-09 Last modified: 2021-06-06

Priority: 2

View all other issues in [concept.default.init].

View all issues with C++20 status.

Discussion:

DefaultConstructible<T> is equivalent to Constructible<T> (18.4.11 [concept.constructible]), which is equivalent to is_constructible_v<T> (21.3.5.4 [meta.unary.prop]). Per 21.3.5.4 [meta.unary.prop] paragraph 8:

The predicate condition for a template specialization is_­constructible<T, Args...> shall be satisfied if and only if the following variable definition would be well-formed for some invented variable t:

T t(declval<Args>()...);
DefaultConstructible<T> requires that objects of type T can be value-initialized, rather than default-initialized as intended.

The library needs a constraint that requires object types to be default-initializable: the "rangified" versions of the algorithms in 26.11.3 [uninitialized.construct.default] proposed in P0896 "The One Ranges Proposal", for example. Users will also want a mechanism to provide such a constraint, and they're likely to choose DefaultConstructible despite its subtle unsuitability.

There are two alternative solutions: (1) change DefaultConstructible to require default-initialization, (2) change is_default_constructible_v to require default-initializaton and specify the concept in terms of the trait. (2) is probably too breaking a change to be feasible.

[2018-08-20 Priority set to 2 after reflector discussion]

Previous resolution [SUPERSEDED]:

  1. Modify [concept.defaultconstructible] as follows:

    template<class T>
      concept DefaultConstructible = Constructible<T> && see below;
    

    -?- Type T models DefaultConstructible only if the variable definition

    T t;
    
    is well-formed for some invented variable t. Access checking is performed as if in a context unrelated to T. Only the validity of the immediate context of the variable initialization is considered.

[2018-08-23 Tim provides updated P/R based on Batavia discussion]

[2018-10-28 Casey expands the problem statement and the P/R]

During Batavia review of P0896R3, Tim Song noted that {} is not necessarily a valid initializer for a DefaultConstructible type. In this sample program (see Compiler Explorer):

struct S0 { explicit S0() = default; };
struct S1 { S0 x; }; // Note: aggregate
S1 x;   // Ok
S1 y{}; // ill-formed; copy-list-initializes x from {}
S1 can be default-initialized, but not list-initialized from an empty braced-init-list. The consensus among those present was that DefaultConstructible should prohibit this class of pathological types by requiring that initialization form to be valid.

[2019 Cologne Wednesday night]

Status to Ready

[2019-10-07 Casey rebases P/R onto N4830 and incorporates WG21-approved changes from P1754R1]

Previous resolution [SUPERSEDED]:

This wording is relative to N4762.

  1. Modify [concept.defaultconstructible] as follows:

    template<class T>
      inline constexpr bool is-default-initializable = see below; // exposition only
    
    template<class T>
      concept DefaultConstructible = Constructible<T> && requires { T{}; } && is-default-initializable<T>;
    

    -?- For a type T, is-default-initializable<T> is true if and only if the variable definition

    T t;
    
    is well-formed for some invented variable t; otherwise it is false. Access checking is performed as if in a context unrelated to T. Only the validity of the immediate context of the variable initialization is considered.

P1754R1 "Rename concepts to standard_case for C++20" - as approved by both LEWG and LWG in Cologne - contained instructions to rename the DefaultConstructible concept to default_initializable "If LWG 3151 is accepted." 3151 is the unrelated "ConvertibleTo rejects conversion from array and function types"; this issue is intended by P1754R1. Since P1754R1 was applied to the working draft in Cologne, whereas this issue was only made Ready, we should apply the desired renaming to the P/R of this issue.

Previous resolution [SUPERSEDED]:

This wording is relative to N4830.

  1. Modify 18.3 [concepts.syn], header <concepts> synopsis, as indicated:

    […]
    //  [concept.defaultconstructible], concept default_constructibleinitializable
    template<class T>
    concept default_constructibleinitializable = see below;
    […]
    
  2. Modify [concept.defaultconstructible] as indicated:

    18.4.12 Concept default_constructibleinitializable [concept.defaultconstructibleinitializable]

    template<class T>
      inline constexpr bool is-default-initializable = see below; // exposition only
    
    template<class T>
      concept default_constructibleinitializable = constructible_from<T> && requires { T{}; } && is-default-initializable<T>;
    

    -?- For a type T, is-default-initializable<T> is true if and only if the variable definition

    T t;
    
    is well-formed for some invented variable t; otherwise it is false. Access checking is performed as if in a context unrelated to T. Only the validity of the immediate context of the variable initialization is considered.

  3. Modify 18.6 [concepts.object] as indicated:

    -1- This subclause describes concepts that specify the basis of the value-oriented programming style on which the library is based.

    template<class T>
    concept movable = is_object_v<T> && move_constructible<T> &&
    […]
    template<class T>
    concept semiregular = copyable<T> && default_constructibleinitializable<T>;
    […]
    
  4. Modify 20.2.2 [memory.syn], header <memory> synopsis, as indicated:

    […]
    namespace ranges {
      template<no-throw-forward-iterator I, no-throw-sentinel<I> S>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_default_construct(I first, S last);
      template<no-throw-forward-range R>
        requires default_constructibleinitializable<range_value_t<R>>
          safe_iterator_t<R> uninitialized_default_construct(R&& r);
    
      template<no-throw-forward-iterator I>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_default_construct_n(I first, iter_difference_t<I> n);
    }
    […]
    namespace ranges {
      template<no-throw-forward-iterator I, no-throw-sentinel<I> S>
        requires default_constructibleinitializable<iter_value_t<I>>
         I uninitialized_value_construct(I first, S last);
      template<no-throw-forward-range R>
        requires default_constructibleinitializable<range_value_t<R>>
          safe_iterator_t<R> uninitialized_value_construct(R&& r);
    
      template<no-throw-forward-iterator I>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_value_construct_n(I first, iter_difference_t<I> n);
    }
    […]
    
  5. Modify 26.11.3 [uninitialized.construct.default] as indicated:

    namespace ranges {
      template<no-throw-forward-iterator I, no-throw-sentinel<I> S>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_default_construct(I first, S last);
      template<no-throw-forward-range R>
        requires default_constructibleinitializable<range_value_t<R>>
          safe_iterator_t<R> uninitialized_default_construct(Ramp;& r);
    }
    

    -2- Effects: Equivalent to:

    […]

    namespace ranges {
      template<no-throw-forward-iterator I>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_default_construct_n(I first, iter_difference_t<I> n);
    }
    

    -4- Effects: Equivalent to:

    […]

  6. Modify 26.11.4 [uninitialized.construct.value] as indicated:

    namespace ranges {
      template<no-throw-forward-iterator I, no-throw-sentinel<I> S>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_value_construct(I first, S last);
      template<no-throw-forward-range R>
        requires default_constructibleinitializable<range_value_t<R>>
          safe_iterator_t<R> uninitialized_value_construct(R&& r);
    }
    

    -2- Effects: Equivalent to:

    […]

    namespace ranges {
      template<no-throw-forward-iterator I>
        requires default_constructibleinitializable<iter_value_t<I>>
          I uninitialized_value_construct_n(I first, iter_difference_t<I> n);
    }
    

    -4- Effects: Equivalent to:

    […]

  7. Modify [range.semi.wrap] as indicated:

    -1- Many types in this subclause are specified in terms of an exposition-only class template semiregular-box. semiregular-box<T> behaves exactly like optional<T> with the following differences:

    1. (1.1) — […]

    2. (1.2) — If T models default_constructibleinitializable, the default constructor of semiregular-box<T> is equivalent to:

      constexpr semiregular-box() noexcept(is_nothrow_default_constructible_v<T>)
        : semiregular-box{in_place}
      { }
      
    3. (1.3) — […]

  8. Modify 25.6.6.2 [range.istream.view], Class template basic_istream_view synopsis, as indicated:

    namespace std::ranges {
      […]
      template<movable Val, class CharT, class Traits>
        requires default_constructibleinitializable<Val> &&
          stream-extractable<Val, CharT, Traits>
      class basic_istream_view : public view_interface<basic_istream_view<Val, CharT, Traits>> {
        […]
      }
      […]
    }
    

[2019-11-17; Daniel comments and restores wording]

During the Belfast 2019 meeting the concept renaming was not voted in by this issue, but separately, the accepted wording can be found in P1917R0#3149. To prevent confusion, the here presented proposed wording has been synchronized with that of the voted in document.

Proposed resolution:

This wording is relative to N4762.

  1. Modify [concept.defaultconstructible] as follows:

    template<class T>
      inline constexpr bool is-default-initializable = see below; // exposition only
    
    template<class T>
      concept DefaultConstructible = Constructible<T> && requires { T{}; } && is-default-initializable<T>;
    

    -?- For a type T, is-default-initializable<T> is true if and only if the variable definition

    T t;
    
    is well-formed for some invented variable t; otherwise it is false. Access checking is performed as if in a context unrelated to T. Only the validity of the immediate context of the variable initialization is considered.