チュートリアル:非テンプレート関数を条件付きで無効にする

T のパラメーターを取る関数テンプレートがあるとします。 .関数テンプレートが operator== のような一般的な名前の場合 、コンストラクター、または他の関数をさらに制約するために型特性を使用して存在を照会される可能性のあるものはすべて、型に必要なプロパティがない場合に関数を条件付きで無効にできると便利なことがよくあります。そうでない場合、関数は「貪欲」になります。必要以上に受け入れる - 一部の特性は、存在をチェックするだけでエラーは後でしか発生しないため、ほぼ役に立たなくなります。

テンプレート パラメーターが特定のプロパティを満たさない場合に関数を条件付きで削除することは、SFINAE で行われます。しかし、それ自体がテンプレートではないクラス テンプレートのメンバー関数がある場合はどうなるでしょうか?

変更され、非常に単純化された std::unique_ptr を検討してください 追加のパラメータ AllowNull を取ります .If AllowNull true です 通常版と同じように動作しますが、false の場合 、ポインターは null であってはなりません。

template <bool AllowNull, typename T>
class unique_ptr
{
public:
 unique_ptr() noexcept
 : ptr_(nullptr) {}

 explicit unique_ptr(T* ptr) noexcept
 : ptr_(ptr)
 {
 assert(ptr_); 
 }

 unique_ptr(unique_ptr&& other) noexcept
 : ptr_(other.ptr_)
 {
 other.ptr_ = nullptr;
 }

 ~unique_ptr() noexcept
 {
 delete ptr_; // delete works with nullptr
 }

 unique_ptr& operator=(unique_ptr&& other) noexcept
 {
 unique_ptr tmp(std::move(other));
 swap(*this, tmp);
 return *this;
 }

 friend void swap(unique_ptr& a, unique_ptr& b) noexcept
 {
 std::swap(a.ptr_, b.ptr_);
 }

 explicit operator bool() const noexcept
 {
 return ptr_ != nullptr;
 }

 T& operator*() const noexcept
 {
 assert(ptr_);
 return *ptr_;
 }

 T* operator->() const noexcept
 {
 assert(ptr_);
 return ptr_;
 }

 T* get() const noexcept
 {
 return ptr_;
 }

 void reset() noexcept
 {
 delete ptr_;
 ptr_ = nullptr;
 }

private:
 T* ptr_;
};

これは単純な unique_ptr の完全な実装です 、しかし、それは AllowNull を完全に無視します パラメータ。

null になる可能性のある問題のある操作を考えてみましょう。それらは次のとおりです。

  • reset() メンバー関数
  • デフォルトのコンストラクタ
  • 移動コンストラクタと代入演算子

ポインターを変更する唯一の他の関数は安全です。なぜなら、コンストラクターは null 以外のポインターをアサートし、デストラクタは問題ではなく、swap() であるためです。 unique_ptr のみを受け入れます まったく同じタイプのオブジェクトであるため、null 以外の unique_ptr にのみスワップできます 両方を非 null のままにします。

したがって、これら 4 つのメンバー関数を条件付きで削除するだけで済みます。特殊化は使用したくありません。これは、多くのコードの重複を伴​​う可能性があるためです (ただし、この例では使用されていません)。

パート 1:メンバー関数を無効にする方法

最初に取り組む関数は reset() です .If AllowNull == false 、この関数は存在してはなりません。

SFINAE に精通している場合は、reset() を変更してみてください。 次のようなものへの署名:

auto reset() noexcept
-> std::enable_if_t<AllowNull>
{
 …
}

reset() の戻り型 std::enable_if_t<AllowNull> に変更されました .この型は true を渡す場合にのみ整形式です テンプレート パラメーターとして、2 番目のパラメーターの型になります (void がデフォルトです。ただし、AllowNull の場合 が false の場合、型の形式が正しくないため、関数は無効になっています。

しかし、このアプローチはうまくいきません。

unique_ptr<false, T> をインスタンス化するとすぐに SFINAE は、代入の失敗はエラーではなく、クラスではなく関数の代入の失敗を表します。

また、関数の代入失敗については、関数テンプレートが必要です。reset()

それでは、テンプレートにしましょう:

template <typename Dummy = void>
auto reset() noexcept
-> std::enable_if_t<AllowNull>
{
 …
}

reset() を作成しました Dummy を追加したテンプレート テンプレート パラメータです。実際には必要ないので、デフォルト値を与えます。呼び出し側には何も変わりませんが、テンプレートができたので、すべて問題ないはずですよね?

いいえ、コンパイラは積極的に AllowNull を置き換えることができるためです 値を調べて、型の形式が正しくないことを検出します。

必要なことは、型を従属にすることです Dummy で パラメータです。たとえば次のようなタイプにすることができます:

template <typename Dummy = void>
auto reset() noexcept
-> std::enable_if_t<AllowNull, Dummy>
{
 …
}

std::enable_if_t<Cond, Type> 実際には typename std::enable_if<Cond, Type>::type のエイリアスです 後者はクラス テンプレートであり、独自の型に特化することができます。したがって、一部のユーザーは Dummy を与えることができます 特殊な std::enable_if を持つユーザー定義型の値 .これは、コンパイラが不正な形式であることを熱心に検出できないことを意味するため、SFINAE は機能します。

SFINAE を使用してそのメンバー関数を条件付きで無効にしました。それを呼び出そうとした場合にのみエラーになりますが、「呼び出しに一致する関数がありません」というエラー、別名オーバーロード解決エラーになるため、他のユーザーは SFINAE を使用できます。 reset() の存在を検出する .

パート 2:デフォルト コンストラクターを無効にする方法

AllowNull == false の場合、デフォルトのコンストラクターも無効にします。 reset() で行ったのと同じことをやってみましょう。 :

template <typename Dummy = void, typename Dummy2 = std::enable_if_t<AllowNull, Dummy>>
unique_ptr()
…

コンストラクターには戻り値の型がないため、std::enable_if_t を使用します。 2 番目のダミー テンプレート パラメータの型として。

そして、これは機能します!

デフォルトのコンストラクターは、引数なしで呼び出し可能なものです。このコンストラクターは - すべてがデフォルト設定されているためです。さらに、std::enable_if_t のテンプレートです。 そのパラメータに依存するため、熱心な置換は行われず、代わりに SFINAE.

パート 3:コピー/ムーブ コンストラクター/割り当てを無効にする方法

削除する必要がある関数は、ムーブ コンストラクターと代入演算子だけです。前の手法は非常にうまく機能したので、ムーブ コンストラクターに適用してみましょう。

template <typename Dummy = void, typename = std::enable_if_t<AllowNull, Dummy>>
unique_ptr(unique_ptr&& other)
…

それでは試してみましょう:

unique_ptr<false, int> a(new int(4));
auto b = std::move(a); // should not compile

しかし、驚くべきことに、このコードはコンパイルされます。実行してみましょう。出力は次のようになります:

*** Error in `./a.out': double free or corruption (fasttop): 0x00000000014f5c20 ***
======= Backtrace: =========
/usr/lib/libc.so.6(+0x70c4b)[0x7f0f6c501c4b]
/usr/lib/libc.so.6(+0x76fe6)[0x7f0f6c507fe6]
/usr/lib/libc.so.6(+0x777de)[0x7f0f6c5087de]
./a.out[0x4006d2]
./a.out[0x400658]
/usr/lib/libc.so.6(__libc_start_main+0xf1)[0x7f0f6c4b1291]
./a.out[0x40053a]
======= Memory map: ========
[…]
Aborted (core dumped)

うーん、変ですね。

clang は、コンパイル時に次の警告を出します:

warning: definition of implicit copy constructor for
 'unique_ptr<false, int>' is deprecated because it has a user-declared
 destructor [-Wdeprecated]
 ~unique_ptr() noexcept

どうやら - 利用可能なムーブ コンストラクターがなかったので - コンパイラーはとても親切で、私たちのためにコピー コンストラクターを生成してくれました。

delete にしましょう コピー操作:

unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

上記のサンプル コードはコンパイルされません。

しかし、それはエラーメッセージです:

error: call to deleted constructor of 'unique_ptr<false, int>'
 auto b = std::move(a);
 ^ ~~~~~~~~~~~~
file.cpp:34:1: note: 'unique_ptr' has been explicitly marked deleted here
unique_ptr(const unique_ptr&) = delete;

移動コンストラクターではなく、コピー コンストラクターを呼び出そうとしますが、コピーが削除されたと文句を言います!その理由は、C++ 標準の次の段落です:

したがって、コピー/移動コンストラクター/代入演算子をテンプレートにすることはできません。なぜなら、それはもはやコピー/移動コンストラクター/代入演算子ではないからです.しかし、テンプレートにできない場合は、SFINAE を使用できません.

何をするつもりですか?部分的な専門化を解決する必要がありますか?

はい、ありますが、unique_ptr 全体を部分的に特殊化する必要はありません。 .前回の投稿では、間接的なレイヤーを追加することでうまくいきました。もう一度やってみましょう.

コンストラクタ/代入/デストラクタの移動を別のクラス unique_ptr_storage に外注します :

namespace detail
{
 template <typename T>
 class unique_ptr_storage
 {
 public:
 unique_ptr_storage(T* ptr) noexcept
 : ptr_(ptr) {}

 unique_ptr_storage(unique_ptr_storage&& other) noexcept
 : ptr_(other.ptr_)
 {
 other.ptr_ = nullptr;
 }

 ~unique_ptr_storage() noexcept
 {
 delete ptr_;
 }

 unique_ptr_storage& operator=(unique_ptr_storage&& other) noexcept
 {
 unique_ptr_storage tmp(std::move(other));
 swap(tmp, *this);
 return *this;
 }

 friend void swap(unique_ptr_storage& a, unique_ptr_storage& b) noexcept
 {
 std::swap(a.ptr_, b.ptr_);
 }

 T* get_pointer() const noexcept
 {
 return ptr_;
 }

 private:
 T* ptr_;
 };
}

実際の unique_ptr ポインターの代わりにこのクラスを保存するようになりました。As unique_ptr_storage 特別なメンバー関数 unique_ptr を定義します それらの定義はもう必要ありません。デフォルトのバージョンで問題ありません。

しかし今では、コンパイラをだましてそれらを生成しないようにすることができます。そのために必要なのは、単純なヘルパー基本クラスだけです:

namespace detail
{
 template <bool AllowMove>
 struct move_control;

 template <>
 struct move_control<true>
 {
 move_control() noexcept = default;

 move_control(const move_control&) noexcept = default;
 move_control& operator=(const move_control&) noexcept = default;

 move_control(move_control&&) noexcept = default;
 move_control& operator=(move_control&&) noexcept = default;
 };

 template <>
 struct move_control<false>
 {
 move_control() noexcept = default;

 move_control(const move_control&) noexcept = default;
 move_control& operator=(const move_control&) noexcept = default;

 move_control(move_control&&) noexcept = delete;
 move_control& operator=(move_control&&) noexcept = delete;
 };
}

次に unique_ptr move_control<true> のいずれかから継承する必要があります または move_control<false>AllowNull に応じて :

template <bool AllowNull, typename T>
class unique_ptr
: detail::move_control<AllowNull>
{
…
};

AllowNull == true の場合 、コンパイラは移動操作を生成できます。しかし、それが false の場合 、基本クラスは移動可能ではないため、できません。そのため、メンバー関数は使用できません。

結論

クラス テンプレートのテンプレート化されていないメンバー関数があり、条件付きでそれを削除したい場合、SFINAE を直接使用することはできません。ダミーのテンプレート パラメーターを追加し、SFINAE 式を作成して、最初に関数をテンプレートにする必要があります。何らかの形でそれに依存しています。

このアプローチは、コピー/移動操作を除くすべてのメンバー関数で機能します。これは、それらがテンプレートになることは決してないためです.カスタムのコピー/移動操作が必要な場合は、別のヘルパー クラスに記述して、クラスで自動的に生成されるようにする必要があります。 .それらを無効にするには、単純に非コピー/移動可能な型から継承します.コンパイラはそれらを自動的に生成できなくなり、それらを削除します.

この例では、部分的なテンプレートの特殊化 (または完全に別の型) で問題をより適切に解決できますが、コードの重複が多すぎる場合があります。同様の手法を使用する必要がある例は、次の std::optionalstd::variant 基になる型がコピー/移動可能でない場合、コピー/移動操作を提供してはなりません。

付録:ドキュメントの生成

しかし今では、次のようなデフォルトのテンプレートを持つ奇妙なメンバー関数がたくさんあります:

template <typename Dummy = void, typename = std::enable_if_t<AllowNull, Dummy>>
void reset();

署名を抽出して出力に使用するドキュメンテーション生成を使用すると、このすべてのノイズが追加されます!

ありがたいことに、私は C++ 用に設計されたドキュメント ジェネレーターである標準化に取り組んできました。これを使用すると、次のマークアップを追加できます。

/// Here be documentation.
/// \param Dummy
/// \exclude
/// \param 1
/// \exclude
template <typename Dummy = void, typename = std::enable_if_t<AllowNull, Dummy>>
void reset();

これにより、出力から 2 つのテンプレート パラメーターが除外されます。関数にはテンプレート パラメーターがないため、標準語はそれがテンプレートであるという事実を黙って隠し、意図した署名のみを文書化します。

void reset();

高度な C++ ドキュメント ジェネレーターが必要な場合は、標準化を試すか、その最新機能の詳細をお読みください。