`__perfect_forward` の仕組みと使い方
概要
- libc++ の
__perfect_forwardの仕組みと使い方について調べた。 -
__perfect_forwardは perfect forwarding call wrapper を実現する基底クラスである。 - perfect forwarding call wrapper は自身の const・reference 修飾を完全転送する call wrapper である。
動機
標準ライブラリの関数オブジェクトを返す関数をよく使いますか? 例えば std::not_fn、 std::bind_front などです[1]。C++23 では std::bind_back がワーキングドラフト入りを目指しています[2]。このような関数の返す関数オブジェクトはどのように実装されているのでしょう。
例として llvm の libc++ を除いてみたところ、 __perfect_forward というクラスに遭遇しました。このクラスがよく出来ていると思い、使い方を調べたのが本記事です。
__perfect_forward の目指すところ
関数オブジェクト、例えば std::not_fn の返す関数オブジェクトである not_fn_t を正しく定義するには、以下のように書く必要があります。
template<class F>
struct not_fn_t {
F f;
template<class... Args>
constexpr auto operator()(Args&&... args) &
noexcept(noexcept(!std::invoke(f, std::forward<Args>(args)...)))
-> decltype(!std::invoke(f, std::forward<Args>(args)...))
{
return !std::invoke(f, std::forward<Args>(args)...);
}
template<class... Args>
constexpr auto operator()(Args&&... args) const&
noexcept(noexcept(!std::invoke(f, std::forward<Args>(args)...)))
-> decltype(!std::invoke(f, std::forward<Args>(args)...))
{
return !std::invoke(f, std::forward<Args>(args)...);
}
template<class... Args>
constexpr auto operator()(Args&&... args) &&
noexcept(noexcept(!std::invoke(std::move(f), std::forward<Args>(args)...)))
-> decltype(!std::invoke(std::move(f), std::forward<Args>(args)...))
{
return !std::invoke(std::move(f), std::forward<Args>(args)...);
}
template<class... Args>
constexpr auto operator()(Args&&... args) const&&
noexcept(noexcept(!std::invoke(std::move(f), std::forward<Args>(args)...)))
-> decltype(!std::invoke(std::move(f), std::forward<Args>(args)...))
{
return !std::invoke(std::move(f), std::forward<Args>(args)...);
}
};
Reference: cppreference.com
似たコードを12回もタイプしなければなりません。さらに、*this が lvalue か rvalue かによってオーバーロードが少し異なります。
しかし、 __perfect_forward を用いると、以下のように書けます。
struct not_fn_op {
template <class... Args>
constexpr auto operator()(Args&&... args) const
noexcept(noexcept(!std::invoke(std::forward<Args>(args)...)))
-> decltype( !std::invoke(std::forward<Args>(args)...))
{ return !std::invoke(std::forward<Args>(args)...); }
};
template <class F>
struct not_fn_t : __perfect_forward<not_fn_op, F> {
using __perfect_forward<not_fn_op, F>::__perfect_forward;
};
Reference: llvm (partially modified)
繰り返しが3回に減りました[3]🤗 このように関数オブジェクトの operator() のオーバーロードを一元化しつつ、引数の完全転送を正しく行うことが __perfect_forward の目指すところです。
__perfect_forward の実装
__perfect_forward は以下のように実装されています。
template <class Op, class Indices, class ...Bound>
struct __perfect_forward_impl;
template <class Op, std::size_t ...Idx, class ...Bound>
struct __perfect_forward_impl<Op, std::index_sequence<Idx...>, Bound...> {
private:
std::tuple<Bound...> bound_;
public:
template <class ...BoundArgs, class = std::enable_if_t<
std::is_constructible_v<std::tuple<Bound...>, BoundArgs&&...>
>>
explicit constexpr __perfect_forward_impl(BoundArgs&& ...bound)
: bound_(std::forward<BoundArgs>(bound)...)
{ }
__perfect_forward_impl(__perfect_forward_impl const&) = default;
__perfect_forward_impl(__perfect_forward_impl&&) = default;
__perfect_forward_impl& operator=(__perfect_forward_impl const&) = default;
__perfect_forward_impl& operator=(__perfect_forward_impl&&) = default;
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound&..., Args...>>>
constexpr auto operator()(Args&&... args) &
noexcept(noexcept(Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...); }
template <class ...Args, class = std::enable_if_t<!std::is_invocable_v<Op, Bound&..., Args...>>>
auto operator()(Args&&...) & = delete;
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound const&..., Args...>>>
constexpr auto operator()(Args&&... args) const&
noexcept(noexcept(Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...); }
template <class ...Args, class = std::enable_if_t<!std::is_invocable_v<Op, Bound const&..., Args...>>>
auto operator()(Args&&...) const& = delete;
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound..., Args...>>>
constexpr auto operator()(Args&&... args) &&
noexcept(noexcept(Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...); }
template <class ...Args, class = std::enable_if_t<!std::is_invocable_v<Op, Bound..., Args...>>>
auto operator()(Args&&...) && = delete;
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound const..., Args...>>>
constexpr auto operator()(Args&&... args) const&&
noexcept(noexcept(Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...); }
template <class ...Args, class = std::enable_if_t<!std::is_invocable_v<Op, Bound const..., Args...>>>
auto operator()(Args&&...) const&& = delete;
};
// __perfect_forward implements a perfect-forwarding call wrapper as explained in [func.require].
template <class Op, class ...Args>
using __perfect_forward = __perfect_forward_impl<Op, std::index_sequence_for<Args...>, Args...>;
Reference: llvm (partially modified)
テンプレート引数・メンバ関数
template <class Op, class Indices, class ...Bound>
struct __perfect_forward_impl;
template <class Op, std::size_t ...Idx, class ...Bound>
struct __perfect_forward_impl<Op, std::index_sequence<Idx...>, Bound...> {
private:
std::tuple<Bound...> bound_;
};
template <class Op, class ...Args>
using __perfect_forward = __perfect_forward_impl<Op, std::index_sequence_for<Args...>, Args...>;
Op はオリジナルの関数オブジェクト型 (function object type) を表します。 not_fn_t の例では not_fn_op に対応します。 std::index_sequence<Idx...> は operator() において std::tuple<Bound...> 型である bound_ を展開するインデックスリストです。 Bound... は一時的に保存する変数の型であり、その変数は std::tuple<Bound...> 型である bound_ に束縛されます。
Bound... は主に cv・reference 修飾の無い型を想定しています。このことは llvm の例 における std::not_fn おいて、 not_fn_t の初期化が下記のように not_fn_t<std::decay_t<F>> と行われていることからもわかります。
template <class F, class = std::enable_if_t<
std::is_constructible_v<std::decay_t<F>, F> &&
std::is_move_constructible_v<std::decay_t<F>>
>>
constexpr auto not_fn(F&& f) {
return not_fn_t<std::decay_t<F>>(std::forward<F>(f));
}
Reference: llvm (partially modified)
not_fn_t<F> と初期化した場合、 not_fn の引数に lvalue を渡すと F は lvalue-reference となります。このとき not_fn_t の operator() が先ほど渡した lvalue の寿命より後に呼ばれると、dangling reference を引き起こし危険です。これが Bound が主に cv・reference 修飾の無い型を想定している理由となります。
// Bad example
template <class F, class = std::enable_if_t<
std::is_constructible_v<std::decay_t<F>, F> &&
std::is_move_constructible_v<std::decay_t<F>>
>>
constexpr auto not_fn(F&& f) {
return not_fn_t<F>(std::forward<F>(f));
// ^~~~~~~~~~~ not `not_fn_t<std::decay_t<F>>`
}
int main() {
using true_fn_t = decltype([](auto&&) { return true; });
true_fn_t fn{};
not_fn(fn); // F = true_fn_t& → dangling reference may arise.
not_fn(true_fn_t{}); // F = true_fn_t
}
コンストラクタ
template <class ...BoundArgs, class = std::enable_if_t<
std::is_constructible_v<std::tuple<Bound...>, BoundArgs&&...>
>>
explicit constexpr __perfect_forward_impl(BoundArgs&& ...bound)
: bound_(std::forward<BoundArgs>(bound)...)
{ }
コピーコンストラクタ、ムーブコンストラクタの他に bound_ を初期化するコンストラクタをもちます。 前項の通り Bound... は主に decay された型であるため、 型 Bound... はこのコンストラクタのテンプレート引数 BoundArgs の cv・reference 修飾という情報を保持しません。すなわちこのコンストラクタで bound_ の各要素はコピー初期化もしくはムーブ初期化されます。
operator()
operator() は一時オブジェクト Op() の operator() の引数に、一時的に保存した変数 bound_ と今回受け取った変数 args... を転送します。動作はこれだけですが、 *this の const・reference 修飾が & 、const& 、&& 、const&& の4通りに分岐してオーバーロードされています。const 修飾の有無に関するオーバーロードの差異は直感的に理解できますが、lvalue-reference か rvalue-reference かに関するオーバーロードの差異は理解し難いので詳しく取り上げます。
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound&..., Args...>>>
constexpr auto operator()(Args&&... args) &
noexcept(noexcept(Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...); }
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound..., Args...>>>
// ^~~~~~~~ difference (1)
constexpr auto operator()(Args&&... args) &&
noexcept(noexcept(Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(std::move(bound_))..., std::forward<Args>(args)...); }
// ^~~~~~~~~~~~~~~~~ difference (2)
operator() の挙動は C++20 規格書 20.14.3項 [func.require] の perfect forwarding call wrapper に記載されています。
A perfect forwarding call wrapper is an argument forwarding call wrapper that forwards its state entities to the underlying call expression. This forwarding step delivers a state entity of type
Tas cvT&when the call is performed on an lvalue of the call wrapper type and as cvT&&otherwise, where cv represents the cv-qualifiers of the call wrapper and where cv shall be neithervolatilenorconst volatile.
Reference: ISO C++ standards committee
perfect forwarding call wrapper は自身が (const)& のときは引数型 T を (const) T として転送し、自身が (const)&& のときは引数型を (const) T&& として転送します。ここで perfect forwarding と表現しているのは call wrapper の const・reference 修飾を完全転送することを意味しており、 bound_ に束縛する前のもとの引数型(例えば BoundArgs...)の const・reference 修飾を完全転送することを意味していません。
std::get の宣言は C++20 規格書 20.5.7項 [tuple.elem] に記載されています。
template<size_t I, class... Types>
constexpr tuple_element_t<I, tuple<Types...>>&
get(tuple<Types...>& t) noexcept;
template<size_t I, class... Types>
constexpr tuple_element_t<I, tuple<Types...>>&&
get(tuple<Types...>&& t) noexcept;
template<size_t I, class... Types>
constexpr const tuple_element_t<I, tuple<Types...>>&
get(const tuple<Types...>& t) noexcept;
template<size_t I, class... Types>
constexpr const tuple_element_t<I, tuple<Types...>>&&
get(const tuple<Types...>&& t) noexcept;
Reference: ISO C++ standards committee
std::get に lvalue である bound_ を渡せば std::get<Idx>(bound_)... の型は Bound&... となります。一方 std::get に rvalue である std::move(bound_) を渡せば std::get<Idx>(std::move(bound_))... の型は Bound&&... となります。すなわち std::move の有無と std::get を組み合わせることで perfect forwarding call wrapper の動作が実現されています。
template <class ...Args, class = std::enable_if_t<!std::is_invocable_v<Op, Bound&..., Args...>>>
auto operator()(Args&&...) & = delete;
// ... and other three overloads
operator() に必要な制約 (例えば std::is_invocable_v<Op, Bound&..., Args...>) が満たされない場合 operator() は delete 指定されます[4]。
__perfect_forward の使い方
以上でも std::not_fn を例に使い方を説明しましたが、ここでは新たに operator+ の第一引数に変数を部分適用して得られる関数 partially_applied_plus を実装します。
int main() {
constexpr auto fn = partially_applied_plus(42);
std::cout << fn(1) << std::endl; // 43
}
手順1: 元となる関数オブジェクト型を実装する
引数の転送先となる関数オブジェクト型である、 partially_applied_plus_op を実装します。この関数オブジェクト型は状態を保持しないため、 operator() は *this が const の場合のみオーバーロードすれば十分です。例外指定と後置戻り値型も正しく書きます。
struct partially_applied_plus_op {
template <class T, class U>
constexpr auto operator()(T&& t, U&& u) const noexcept(
noexcept( std::forward<T>(t) + std::forward<U>(u)))
-> decltype(std::forward<T>(t) + std::forward<U>(u)) {
return std::forward<T>(t) + std::forward<U>(u);
}
};
説明のため実装しましたが、 partially_applied_plus_op は std::plus<> を使えば十分です。
using partially_applied_plus_op = std::plus<>;
手順2: perfect forwarding call wrapper 型を実装する
perfect forwarding call wrapper に従い引数を完全転送する関数オブジェクト型を実装します。 __perfect_forward<partially_applied_plus_op, T> を public 継承することで operator= と operator() を宣言します。さらに継承コンストラクタ (using __perfect_forward<partially_applied_plus_op, T>::__perfect_forward) を用いて基底クラスのコンストラクタを暗黙的に宣言します。
template <class T>
struct partially_applied_plus_t : __perfect_forward<partially_applied_plus_op, T> {
using __perfect_forward<partially_applied_plus_op, T>::__perfect_forward;
};
手順3: perfect forwarding call wrapperを使う
最後に perfect forwarding call wrapper を宣言し、使ってみます。上記の通り T は decay された型を使うことに注意します。std::decay_t<T> のテンプレート型制約は (元の関数オブジェクトが呼び出し可能な条件を満たすことのほかに) 少なくとも T 型より構築可能かつムーブ構築可能である必要があります。
template <class T, class = std::enable_if_t<
std::is_constructible_v<std::decay_t<T>, T> &&
std::is_move_constructible_v<std::decay_t<T>>
>>
constexpr auto partially_applied_plus(T&& t) {
return partially_applied_plus_t<std::decay_t<T>>(std::forward<T>(t));
}
以上のコードの Wandbox における実行結果 を置いておきます。
使用の際の注意点
もう一度 operator() の実装を示します。
template <class ...Args, class = std::enable_if_t<std::is_invocable_v<Op, Bound&..., Args...>>>
constexpr auto operator()(Args&&... args) &
noexcept(noexcept(Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...)))
-> decltype( Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...))
{ return Op()(std::get<Idx>(bound_)..., std::forward<Args>(args)...); }
// ^~~~ Note!
// ... and other three overloads
Op がデフォルト構築されています。すなわち Op はデフォルト構築可能でなければなりません。デフォルト構築できない関数オブジェクトとは何でしょうか。それは例えば、キャプチャをもつラムダ式です。
C++20 よりキャプチャをもたないラムダ式がデフォルト構築可能、代入可能となりました[5] [6]。さらに評価されない文脈でのラムダ式の記述が可能となりました[7] [8]。しかしキャプチャをもたないという制約を忘れると、以下のように書いてしまうかもしれません。
// Bad example
template <class Op, class ...Args>
struct perfect_forwarded_t : __perfect_forward<Op, Args...> {
using __perfect_forward<Op, Args...>::__perfect_forward;
};
int main() {
using affine_transform_op = decltype([b = 1.0](double a, double x) { return a * x + b; });
perfect_forwarded_t<affine_transform_op, double> affine_transform(2.0);
std::cout << affine_transform(3.0) << std::endl; // error!
}
しかしこのコードは affine_transform の関数呼び出しに失敗します (Wandbox における実行結果) 。なぜならキャプチャをもつラムダ式型はデフォルト構築可能ではないからです。operator() の呼び出しの際 Op のデフォルト構築に失敗するため、目的の operator() がオーバーロード候補から除外されます。
まとめ
- libc++ の
__perfect_forwardは自身の const・reference 修飾を完全転送する call wrapper を実現するクラスである。 -
__perfect_forwardを使う際は元となる関数オブジェクト型を用意し、__perfect_forwardを用いて perfect forwarding call wrapper 型を実装する。 -
Bound...は decay された型である必要がある、Opはデフォルト構築可能である必要があるといったことに注意。
謝辞
この記事は llvm に inspire されて書かれました。また以下の方々の記事は日頃から参考にさせていただいております。お礼申し上げます (敬称略) 。
- yohhoyの日記
- 地面を見下ろす少年の足蹴にされる私
- @Reputeless 特に C++ の歩き方 | cppmap
- @_EnumHack 特に C++のパラメータパック基礎&パック展開テクニック
この記事が私にとって最初のアウトプット記事になります。至らないところがございましたらコメント頂けますと幸いです。
最後にここまでお付き合いくださった読者の皆様に感謝申し上げます。
-
https://en.cppreference.com/w/cpp/utility/functional "Function objects - cppreference.com" ↩︎
-
https://wg21.link/p2387 "P2387" ↩︎
-
この3回の繰り返しを1回に減らすための提案 P0573 がありましたが reject されました。reject された経緯は Why were abbrev. lambdas rejected? にあります。さらにこの提案を brush up した提案 P2425 がなされています。この提案の日本語の解説記事が [C++]WG21月次提案文書を眺める(2021年08月) - 地面を見下ろす少年の足蹴にされる私 にあります。 ↩︎
-
ここでは
std::enable_ifを用いて型制約を実現しています。C++20 からは requires 節 を用いてより簡潔に制約を記述できます。 ↩︎ -
https://wg21.link/p0624 "P0624" ↩︎
-
https://cpprefjp.github.io/lang/cpp20/default_constructible_and_assignable_stateless_lambdas.html "cpprefjp" ↩︎
-
https://wg21.link/p0315 "P0315" ↩︎
-
https://cpprefjp.github.io/lang/cpp20/wording_for_lambdas_in_unevaluated_contexts.html "cpprefjp" ↩︎
Discussion