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_constructible
はtrue
なのに実際のコンパイルには失敗する、という状況になります。
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_v
がtrue
になる条件がゆるくないかとは思いますが、そういう仕様なので仕方ありません。
おかしな挙動だと思うのは、このときにstd::is_constructible
はtrue
になることです。
//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のケースよりはそれほどおかしいとは思っていません。