LoginSignup
5
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-02

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

問題点

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

// アロケータを使って構築するクラス
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_constructibletrueなのに実際のコンパイルには失敗する、という状況になります。

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++規格書によると、

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の定義は簡単です。

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つ目のケース

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関数を使って構築することができます。

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を引数にとるコンストラクタで構築できません。

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

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

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つ目のケース

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関数にわたすとコンパイルエラーになります。

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

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

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

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

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

    //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のケースよりはそれほどおかしいとは思っていません。

5
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4