以下は、適切に制約されたコンストラクター テンプレートを作成するさまざまな方法です。複雑さの昇順、対応する機能の豊富さの昇順、落とし穴の数の降順で示します。
この特定の形式の EnableIfが使用されますが、これは実装の詳細であり、ここで概説されている手法の本質を変更しません。また、異なるメタ計算を組み合わせるためのAnd
およびエイリアスがあることも想定されています。Not
例えばAnd<std::is_integral<T>, Not<is_const<T>>>
よりも便利ですstd::integral_constant<bool, std::is_integral<T>::value && !is_const<T>::value>
。
コンストラクター テンプレートに関しては、制約がまったくないよりも、制約がある方がはるかに優れているため、特定の戦略はお勧めしません。可能であれば、非常に明白な欠点を持つ最初の 2 つの手法は避けてください。残りの手法は、同じテーマに関する詳細な説明です。
自分自身を拘束する
template<typename T>
using Unqualified = typename std::remove_cv<
typename std::remove_reference<T>::type
>::type;
struct foo {
template<
typename... Args
, EnableIf<
Not<std::is_same<foo, Unqualified<Args>>...>
>...
>
foo(Args&&... args);
};
利点:次のシナリオで、コンストラクターがオーバーロードの解決に参加するのを回避します。
foo f;
foo g = f; // typical copy constructor taking foo const& is not preferred!
欠点:他のすべての種類の過負荷解決に参加します
構築式の制約
foo_impl
コンストラクターにはfromを構築するという道徳的効果があるためArgs
、これらの正確な用語に対する制約を表現するのは自然なことのように思われます。
template<
typename... Args
, EnableIf<
std::is_constructible<foo_impl, Args...>
>...
>
foo(Args&&... args);
利点:これは、何らかのセマンティック条件が満たされた場合にのみオーバーロードの解決に参加するため、正式に制約付きテンプレートになりました。
欠点:以下は有効ですか?
// function declaration
void fun(foo f);
fun(42);
たとえば、foo_impl
がの場合、std::vector<double>
はい、コードは有効です。はそのような型のベクトルを作成std::vector<double> v(42);
する有効な方法であるため、からへの変換は有効です。言い換えれば、他のコンストラクタの問題を脇に置いてください(パラメータの順序が入れ替わっていることに注意してください-それは残念です)。int
foo
std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value
foo
構築式を明示的に制約する
当然、次のことがすぐに思い浮かびます。
template<
typename... Args
, EnableIf<
std::is_constructible<foo_impl, Args...>
>...
>
explicit foo(Args&&... args);
コンストラクターをマークする 2 回目の試行explicit
。
メリット:上記のデメリットを回避!また、それを忘れない限り、それほど時間はかかりませんexplicit
。
欠点:foo_impl
がの場合std::string
、次のことが不便な場合があります。
void fun(foo f);
// No:
// fun("hello");
fun(foo { "hello" });
foo
たとえば、 が の薄いラッパーであることを意図しているかどうかによって異なりますfoo_impl
。これが、より厄介な欠点であると私が考えるものfoo_impl
ですstd::pair<int, double*>
。
foo make_foo()
{
// No:
// return { 42, nullptr };
return foo { 42, nullptr };
}
ここで実際に何かから私を救うような気がしませんexplicit
.中括弧には2つの引数があるため、明らかに変換ではなく、型はfoo
すでに署名に表示されているため、そうであると感じたときにそれを惜しみません.冗長。std::tuple
その問題に苦しんでいます(工場std::make_tuple
はその痛みを少し和らげますが)。
建設からの変換を個別に制約する
構築と変換の制約を別々に表現してみましょう:
// New trait that describes e.g.
// []() -> T { return { std::declval<Args>()... }; }
template<typename T, typename... Args>
struct is_perfectly_convertible_from: std::is_constructible<T, Args...> {};
template<typename T, typename U>
struct is_perfectly_convertible_from: std::is_convertible<U, T> {};
// New constructible trait that will take care that as a constraint it
// doesn't overlap with the trait above for the purposes of SFINAE
template<typename T, typename U>
struct is_perfectly_constructible
: And<
std::is_constructible<T, U>
, Not<std::is_convertible<U, T>>
> {};
使用法:
struct foo {
// General constructor
template<
typename... Args
, EnableIf< is_perfectly_convertible_from<foo_impl, Args...> >...
>
foo(Args&&... args);
// Special unary, non-convertible case
template<
typename Arg
, EnableIf< is_perfectly_constructible<foo_impl, Arg> >...
>
explicit foo(Arg&& arg);
};
利点:の構築と変換は、 の構築と変換のfoo_impl
必要十分条件になりましfoo
た。つまりstd::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value
、std::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value
どちらも (ほぼ) 成立します。
欠点? たとえばのfoo f { 0, 1, 2, 3, 4 };
場合は機能しません。これは、制約がスタイルの構築に関するものであるためです。制約のある(読者への演習として残されている)オーバーロード取得をさらに追加することも、さらにはオーバーロード取得(制約も読者への演習として残されている) を追加することも可能ですが、複数からの「変換」であることを覚えておいてください。引数は構造ではありません!)。重複を避けるために変更する必要はないことに注意してください。foo_impl
std::vector<int>
std::vector<int> v(0, 1, 2, 3, 4);
std::initializer_list<T>
std::is_convertible<std::initializer_list<T>, foo_impl>
std::initializer_list<T>, Ts&&...
is_perfectly_convertible_from
私たちの間でより卑劣な人は、狭いコンバージョンを他の種類のコンバージョンと区別するようにします.