std::tupleのallocator_arg_tを引数に取るコンストラクタの仕様はおかしいのではないか

# tuple::tuple(allocator_arg_t, Alloc, ...)

## 問題点

`std::tuple`には数多くのコンストラクタがありますが、それぞれに対して`std::allocator_arg_t`とアロケータを追加で引数に取るオーバーロードがあります。これは次のように使います。

```cpp
// アロケータを使って構築するクラス
struct S
{
    // allocator_type をメンバとして定義するとuses-allocator構築を使うようになる
    using allocator_type = std::allocator<int>;

    // (1)
    S(int);

    // (2)
    S(std::allocator_arg_t, allocator_type const&, int)
    {
        std::cout << "S::S(std::allocator_arg_t, allocator_type const&, int)" << std::endl;
    }
};

int main()
{
    // (2)のコンストラクタが呼び出される
    std::tuple<S> t(std::allocator_arg, std::allocator<int>{}, 10);
    return 0;
}
```

さて、ここで(1)のコンストラクタは宣言だけで定義されておらず、実際に呼び出されることもありませんが、この宣言を削除すると**コンパイルエラーになります**。

また、(2)のコンストラクタを削除すると、`is_constructible`は`true`なのに実際のコンパイルには失敗する、という状況になります。

```cpp
struct S
{
    using allocator_type = std::allocator<int>;

    S(int) {}

    //S(std::allocator_arg_t, allocator_type const&, int)
    //{
    //    std::cout << "S::S(std::allocator_arg_t, allocator_type const&, int)" << std::endl;
    //}
};

int main()
{
    // これはtrueなのに
    static_assert(std::is_constructible_v<
        std::tuple<S>, std::allocator_arg_t, std::allocator<int> const&, int>);
    // これはコンパイルエラー
    std::tuple<S> t(std::allocator_arg, std::allocator<int>{}, 10);
    return 0;
}
```

いったいなぜでしょうか？

## 原因

これの原因は、`allocator_arg_t`とアロケータを引数にとるコンストラクタは、そうでないコンストラクタと同じ条件で制約されているためです。つまり上記の例だと`is_constructible<S, int>`が`true`であるかどうかを見ています。
これは libstdc++, libcxx, Microsoft-STL 全ての実装でそうなっています。

[最新のC++規格書](https://eel.is/c++draft/tuple#cnstr-33)によると、

> Effects: Equivalent to the preceding constructors except that each element is constructed with uses-allocator construction.
>> 各要素が uses-allocator 構築されることを除けば，前述のコンストラクタと同等である。

となっており、実装は規格に適合しているように思われます。

## 疑問

この動作は明らかにおかしいと思うのですが、規格のバグでしょうか？それとも標準ライブラリの各実装が間違っているのでしょうか？または何かしら理由があってこうなっているのでしょうか？

わからないので有識者の方教えてください。

## 解決案

筆者なりに納得行く動作にするには、uses-allocator構築が可能かどうか取得するTypeTraits、`is_uses_allocator_constructible`を追加し、`allocator_arg_t`を取るコンストラクタはそれを使って制約するべきだと思います。
`is_uses_allocator_constructible`の定義は簡単です。

```cpp
template <typename T, typename Alloc, typename... Args>
using is_uses_allocator_constructible =
    std::disjunction<
        std::conjunction<std::uses_allocator<std::remove_cv_t<T>, Alloc>, std::is_constructible<T, Args...>>,
        std::conjunction<std::negation<std::uses_allocator<std::remove_cv_t<T>, Alloc>>, std::is_constructible<T, std::allocator_arg_t, Alloc const&, Args...>>,
        std::conjunction<std::negation<std::uses_allocator<std::remove_cv_t<T>, Alloc>>, std::is_constructible<T, Args..., Alloc const&>>
    >;
/*
(uses_allocator<T, Alloc>::value && is_constructible<T, Args...>) ||
(!uses_allocator<T, Alloc>::value && is_constructible<T, allocator_arg_t, Alloc const&, Args...>) ||
(!uses_allocator<T, Alloc>::value && is_constructible<T, Args..., Alloc const&>)
*/
```

## もしかして

uses-allocator構築なんて誰も使っていない可能性が・・・？

## 追記

Twitterで有識者の方に反応いただいてありがたかったですが、私の言いたいことが100％伝わっていないような気がしたので、改めて説明し直してみました。
言いたいことは上記と同じです。

### 1つ目のケース
```cpp
struct S1
{
    using allocator_type = std::allocator<int>;

    S1(std::allocator_arg_t, allocator_type const&, int)
    {
        std::cout << "S1::S1(std::allocator_arg_t, allocator_type const&, int)" << std::endl;
    }
};
```

S1クラスは、uses-allocator構築することを意図して**正しく**作られたクラスで、`std::make_obj_using_allocator`関数を使って構築することができます。

```cpp
int main()
{
    std::allocator<int> a;
    auto s = std::make_obj_using_allocator<S1>(a, 42);
    return 0;
}
```

しかし、S1を`std::tuple`の要素にしたとき、`std::allocator_arg_t`を引数にとるコンストラクタで構築できません。

```cpp
int main()
{
    std::allocator<int> a;
    std::tuple<S1> t(std::allocator_arg, a, 42);  // コンパイルエラー！
    return 0;
}
```

これがコンパイルエラーにならないようにするためには、S1クラスにアロケータを取らないバージョンのコンストラクタ(`S1::S1(int)`)を追加する必要があります。このコンストラクタは実際には呼び出されないため、宣言だけで大丈夫です。

```cpp
struct S1
{
    using allocator_type = std::allocator<int>;

    S1(int);  // こうするとエラーにならなくなる

    S1(std::allocator_arg_t, allocator_type const&, int)
    {
        std::cout << "S1::S1(std::allocator_arg_t, allocator_type const&, int)" << std::endl;
    }
};
```

S1は uses-allocator 構築することを意図して正しく記述されたクラスにもかかわらず、`std::tuple`の要素にしたときに`std::allocator_arg_t`を取るコンストラクタで構築できない、しかもその回避方法は実際には呼び出されないコンストラクタを宣言する、というのは明らかにおかしいやろ！と思うわけです。

### 2つ目のケース

```cpp
struct S2
{
    using allocator_type = std::allocator<int>;

    S2(int)
    {
        std::cout << "S2::S2(int)" << std::endl;
    }
};
```

S2クラスは、`allocator_type`というtypdefを定義しているにも関わらず、アロケータを引数にとるコンストラクタを定義していません。これはuses-allocator構築の文脈から言うと**間違っている**クラスで、実際に`std::make_obj_using_allocator`関数にわたすとコンパイルエラーになります。

```cpp
int main()
{
    std::allocator<int> a;
    std::make_obj_using_allocator<S2>(a, 42); // コンパイルエラー
    return 0;
}
```

当然、`std::tuple`の要素としたときもuses-allocator構築できません。

```cpp
int main()
{
    std::allocator<int> a;
    std::tuple<S2> t(std::allocator_arg, a, 42); // コンパイルエラー
    return 0;
}
```

これは、S2クラスの定義に問題があるので、納得の挙動です。若干、`std::uses_allocator_v`が`true`になる条件がゆるくないかとは思いますが、そういう仕様なので仕方ありません。

おかしな挙動だと思うのは、このときに`std::is_constructible`は`true`になることです。

```cpp
    //std::tuple<S2> t(std::allocator_arg, a, 42); // これはコンパイルエラーになるのに
    static_assert(std::is_constructible_v<         // これはtrueになる
        std::tuple<S2>, std::allocator_arg_t, std::allocator<int>, int>);
```

例えば、「`std::tuple<T>`が`std::allocator_arg_t`のコンストラクタで構築できたらそっちを使い、そうでない場合はアロケータ無しのコンストラクタで構築する」のような処理を書いている場合は問題になるかもしれません。

ですが、S2の定義にそもそも問題があるので、S1のケースよりはそれほどおかしいとは思っていません。