Title
Uses-allocator construction of `pair` in `tuple`'s `allocator_arg_t` constructors
Status
new
Section
[tuple.cnstr]
Submitter
Jiang An

Created on 2025-08-07.00:00:00 last changed 1 week ago

Messages

Date: 2025-08-17.15:09:01

Proposed resolution:

This wording is relative to N5014.

  1. Modify [tuple.cnstr] as indicated:

    template<class Alloc>
      constexpr explicit(see below )
        tuple(allocator_arg_t, const Alloc& a);
    […]
    template<class Alloc, class U1, class U2>
      constexpr explicit(see below)
        tuple(allocator_arg_t, const Alloc& a, pair<U1, U2>&);
    template<class Alloc, class U1, class U2>
      constexpr explicit(see below)
        tuple(allocator_arg_t, const Alloc& a, const pair<U1, U2>&);
    template<class Alloc, class U1, class U2>
      constexpr explicit(see below)
        tuple(allocator_arg_t, const Alloc& a, pair<U1, U2>&&);
    template<class Alloc, class U1, class U2>
      constexpr explicit(see below)
        tuple(allocator_arg_t, const Alloc& a, const pair<U1, U2>&&);
    template<class Alloc, tuple-like UTuple>
      constexpr explicit(see below)
      tuple(allocator_arg_t, const Alloc& a, UTuple&&);
    

    -32- Preconditions: `Alloc` meets the Cpp17Allocator requirements ([allocator.requirements.general]).

    -33- Effects: Equivalent to the preceding constructors except that each element is constructed with uses-allocator construction ([allocator.uses.construction]), except that the construction behaves as if there were only one `uses_allocator_construction_args` overload and the overload behaved the same as the first actual overload without Constraints.

Date: 2025-08-15.00:00:00

[ 2025-08-17; Pablo comments ]

I don't agree with it or the PR. It seems like the implementations are simply lagging behind. Since `make_obj_using_allocator` is in the standard, `tuple` is easy enough to implement simply by delegating to that function. I regard the failure to handle `pair` in a general way to be a defect in the C++11 specification and that handling it correctly, though technically a change of behavior, is more likely to fix bugs than to create them. It is exactly this scattershot restatement of uses-allocator construction for `pair` that I hoped to fix with P0591, even though I missed `tuple` in my exposition.

Date: 2025-08-17.08:20:23

Before P0591R4, only `scoped_allocator_adaptor::construct` and `polymorphic_allocator::construct` specially handled `pair` for the purpose of uses-allocator construction. The primary definition of uses-allocator construction (in e.g., N4659 [allocator.uses.construction]) did not specially handle `pair`. The `allocator_arg_t` constructors of `tuple`, which were specified to construct `tuple` elements with uses-allocator constructor (per e.g., N4659 [tuple.cnstr] p26), did not specially handle `pair` either.

P0591R4 redefined uses-allocator construction in terms of `make_obj_using_allocator` in [allocator.uses.construction] p1 as:

When applied to the construction of an object of type `T`, it is equivalent to initializing it with the value of the expression make_obj_using_allocator<T>(alloc, args...), described below.

And the new definition does handle `pair`. As the specification of `allocator_arg_t` constructors of `tuple` (now in [tuple.cnstr] p33) still refer to uses-allocator construction as-is, these constructors should construct a `pair` element in a way equivalent to `make_obj_using_allocator` now.

The following example shows the behavioral difference.

#include <cstddef>
#include <utility>
#include <tuple>
#include <memory>
#include <vector>
#include <cassert>

template<class T>
class payload_ator {
  int payload{};
   
public:
  using value_type = T;

  payload_ator() = default;

  constexpr explicit payload_ator(int n) noexcept : payload{n} {}

  template<class U>
  constexpr explicit payload_ator(payload_ator<U> a) noexcept : payload{a.payload} {}  

  friend bool operator==(payload_ator, payload_ator) = default;

  template<class U>
  friend constexpr bool operator==(payload_ator x, payload_ator<U> y) noexcept {
    return x.payload == y.payload;
  }  

  constexpr T* allocate(std::size_t n) { return std::allocator<T>{}.allocate(n); }

  constexpr void deallocate(T* p, std::size_t n) { return std::allocator<T>{}.deallocate(p, n); }  

  constexpr int get_payload() const noexcept { return payload; }
};

bool test() {
  constexpr int in_v = 42;
  using my_pair_t = std::pair<int, std::vector<int, payload_ator<int>>>;
  std::tuple<my_pair_t> t(std::allocator_arg, payload_ator<int>{in_v});
  auto out_v = std::get<0>(t).second.get_allocator().get_payload();
  return in_v == out_v;
}

int main() {
  assert(test()); // passes only if allocator_arg_t constructors of tuple specially handle pair
}

However, the behavioral changes of these constructors were not discussed in P0591R4, and existing implementations that claim full implementation of P0591R4 (MSVC STL and libstdc++) did not change these constructors (demo).

Given that implementations did not recognize changes of `allocator_arg_t` constructors as part of the paper, and special handling of `pair` would significantly complicate these constructors, perhaps we should explicitly specify that these constructors behave as if special handling for `pair` were missing.

History
Date User Action Args
2025-08-17 15:09:01adminsetmessages: + msg14940
2025-08-09 17:21:04adminsetmessages: + msg14931
2025-08-07 00:00:00admincreate