はじめに
この記事は、C++ Advent Calendar 2019 の 8 日目の投稿です。
昨日は @h6akh さんの C++部・ユニットテストの裏技 〜private関数をテストする〜 でした。
明日は @mdstoy さんの next_combinationを実装してみた になります。
C++20 の機能がどんどん実装されてきていますが、この記事では、C++ 全般の少し使えるかもしれないネタをいくつか紹介しようと思います。タイトルの「C++ プログラムを進化させる〇項目」は、Effective C++ シリーズの副題を流用したものです。4 項目の中身は、小ネタ 2 つと、ツイッターで見つけたネタ 2 つです。また、本文中のサンプルコードでは、読みやすさのために constexpr
や noexcept
などを省略しています。
項目 1 : std::forward
にテンプレート引数を指定する理由
完全転送の際に std::forward
を使うとき、いつもテンプレート引数を指定して使っていると思います。ですが、テンプレート引数をこちらで指定しなくても、C++ にはテンプレート引数を推定する機能があるので、これを使えばいいのではないか、という疑問を持つ方もいるのではないでしょうか。しかし実は、forward
にテンプレート引数を指定しないと完全転送がうまくされません。
次のようなコードで、このテンプレート引数を推定する forward
とそうではない std::forward
とを比較してみます。
#include <iostream>
#include <utility>
// forward の、テンプレート引数を推論する実装
template<typename T>
T&& forward_with_deduction(T&& obj)
{
return static_cast<T&&>(obj);
}
// 引数を転送したい関数
auto f(auto&) { std::cout << "mached: &" << '\n';}
auto f(const auto&) { std::cout << "mached: const &" << '\n';}
auto f(auto&&) { std::cout << "mached: &&" << '\n'; }
// テンプレート引数を推論する自作 forward を使って転送
template<typename T>
void forwarder1(T&& obj)
{
f(forward_with_deduction(obj));
}
// std::forward<T> を使って転送
template<typename T>
void forwarder2(T&& obj)
{
f(std::forward<T>(obj));
}
int main()
{
int x;
const int& y(x);
int&& z = std::move(x);
std::cout << "引数の推論あり:\n\n";
// 出力
forwarder1(3); // mached: &
forwarder1(x); // mached: &
forwarder1(y); // mached: const &
forwarder1(z); // mached: &
forwarder1(std::move(x)); // mached: &
forwarder1(std::move(y)); // mached: const &
std::cout << "\n引数の推論なし:\n\n";
// 出力
forwarder2(4); // mached: &&
forwarder2(x); // mached: &
forwarder2(y); // mached: const &
forwarder2(z); // mached: &
forwarder2(std::move(x)); // mached: &&
forwarder2(std::move(y)); // mached: &&
}
すると、テンプレート引数を推論する自作の forward
を使って転送する forwarder1
は、関数の引数を完全転送できない結果となりました。
テンプレート引数の推論規則
このような動作になるのは、自作 forward
のユニヴァーサル参照 T&&
の推論結果が std::forward
にテンプレート引数を渡した際のユニヴァーサル参照T&&
の型と異なることが原因だと考えられます。そこで、まず自作 forward
である forward_with_deduction
のテンプレート引数T
がどのような型に推論されたのかを確かめてみます。テンプレートの型推論の規則は、Effective Modern C++の項目 1 に詳しく記載されていますので、ここではその記述から必要な箇所を引用します。
template<typename T> // from Effective Modern C++
void f(ParamType pram);
f(expr);
上のようなものがあるときに、
テンプレート引数
T
の推論は、expr
の型だけから決定されるのではなく、ParamType
からも影響をうけ、それは次の 3 通りの場合があります。
ケース 1.
ParamType
が参照もしくはポインタだが、ユニヴァーサル参照ではない。
ケース 2.ParamType
がユニヴァーサル参照である。
ケース 3.ParamType
がポインタでも参照でもない。
Effective Modern C++ 第 1 項より
上記のコードのforward_with_deduction
のParamType
に相当する部分は、ケース 2 のユニヴァーサル参照(T&&
)です。このとき、次のように T
が推論されます。
ケース 2 :
expr
が左辺値ならば、T
もParamType
も左辺値参照と推論される。これは 2 つの意味で特殊である。まず、テンプレートの型推論で、T
を参照として推論するのはこの場合だけである。もう 1 つは、ParamType の宣言には右辺値参照という形態をとりながら推論される型は左辺値参照となる点である。expr
が右辺値の場合は、「通常の」規則が適用される(ケース 1)。
同じく、Effective Modern C++ 第 1 項より
冒頭のコードでは、forward_with_deduction(obj)
の関数の引数の式 obj
は変数の名前なので左辺値(lvalue)です。したがって、テンプレート引数 T
は左辺値参照となることが分かります。式 obj
の型は int
(もちろん、変数obj
の型はint&&
です。式の場合は最終的に非参照型をもちます。1)なので、結果としてテンプレート引数T
はint&
と推論されています。
T&&
の型
ユニヴァーサル参照 T&&
型は、T
が左辺値参照型に推論されたときに、参照の圧縮2によって T&
となります。結果として、forward_with_deduction
内で使っている型 T&&
は常に左辺値参照となってしまうため、テンプレート引数を推論するような forward
は書けないということが分かりました。テンプレート引数 T
をこちらから指定すれば、ユニヴァーサル参照 T&&
はテンプレート引数 T
に左辺値参照を渡したときに左辺値参照、そうでないときには右辺値参照となり期待される動作をすることが分かります。
std::forward<T>
と std::forward<decltype(obj)>
に違いはない
ところで、std::forward
のテンプレート引数を渡す際は std::forward<T>
と std::forward<decltype(obj)>
のように 2 通りの指定ができますが、このどちらを指定しても実際の動作に違いはありません。ここで、std::forward
は、例えば gcc では次のように実装されていました。
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
冒頭のコードの、forwarder2(4)
, forwarder2(std::move(x))
, そして forwarder2(std::move(y))
を呼び出す際に、std::forward<T>
のようにテンプレート引数を指定したときに gcc の std::forward
の実装の _Tp
は int
になります。また、std::forward<decltype(obj)>
のように指定した際は _Tp
は int&&
になります。しかし、_Tp&&
はどちらも int&&
となるので、テンプレート引数にT
,decltype(obj)
どちらを指定してもstd::forward
は問題なく動作していることが分かります。
項目 2 : ラムダ式の中でキャプチャした変数を move したいときには、ラムダ式を mutable で修飾する
ラムダ式内で move してもコピーされてしまうの、これ結構やらかしてる気がする https://t.co/TJNS1noGe3
— std::めるぽん (@melponn) June 11, 2019
上記の、めるぽんさんのとても興味深いツイートを紹介します。ラムダ式の中でキャプチャした変数を move するようなときには、ラムダ式に mutable
を指定しないと期待した動作にならないというものです。以下に、ツイートのリンク先のコードと出力を引用させていただきます。
#include <utility>
#include <iostream>
struct A {
A() { std::cout << "A::A()" << std::endl; }
A(const A&) { std::cout << "A::A(const A&)" << std::endl; }
A(A&&) { std::cout << "A::A(A&&)" << std::endl; }
A& operator=(const A&) { std::cout << "A::operator=(const A&)" << std::endl; return *this; }
A& operator=(A&&) { std::cout << "A::operator=(A&&)" << std::endl; return *this; }
};
void f(A) {
std::cout << "f(A)" << std::endl;
}
int main() {
A a;
auto g = [a = std::move(a)]() {
std::cout << "lambda g" << std::endl;
// move したつもりなのにコピーされる
f(std::move(a));
};
g();
auto h = [a = std::move(a)]() mutable {
std::cout << "lambda h" << std::endl;
// これならOK
f(std::move(a));
};
h();
}
A::A()
A::A(A&&)
lambda g
A::A(const A&)
f(A)
A::A(A&&)
lambda h
A::A(A&&)
f(A)
mutable
が使用されていないラムダ式は、operator()
が const
修飾されており、キャプチャしたオブジェクトを変更することができません。上の例コードでは、関数内ではキャプチャしたオブジェクト a
に const
がついているようなものだと考えられます。
const A a;
auto b = std::move(a); // コピー代入が行われる
上のコードのように、const 修飾されたオブジェクトを move しようとしてもコピーされてしまいます。
ラムダ式内でキャプチャした変数を move する際には mutable
を使用すると、期待通りの動作になるようです。
項目 3 : 総称ラムダは、高階関数の引数に便利
高階関数の引数にする際に総称ラムダを用いると、戻り値型や引数型のテンプレート引数を指定しなくても良いので便利です。テンプレート関数を引数に取れるようなテンプレート関数を定義する際などは、テンプレートの定義が煩雑になりがちで、さらに少なくとも戻り値型はこちらから指定する必要があるようです。3 以下に、簡単な比較を載せます。
#include <iostream>
#include <tuple>
#include <functional>
template <typename T, typename ... U>
auto high_order_fn(T && func, U &&... args) // 高階関数
{
return std::invoke(std::forward<T>(func), std::forward<U>(args)...);
}
// 総称ラムダ
auto lambda = [](auto&& x){ std::cout << x << '\n'; };
// テンプレート関数
template <typename Arg>
void fn(Arg&& x)
{
std::cout << x << '\n';
}
int main()
{
high_order_fn(lambda, "Hello, World!");
// high_order_fn(&fn, "Hello, World!"); エラー!
high_order_fn(&fn<std::string>, "Hello, World!"); // この例では、特殊化(実体化したもの)を渡せば OK
}
総称ラムダにコンパイル時引数を持たせる
テンプレート関数の非型テンプレートパラメータで、コンパイル時に関数に引数を取らせることができました。総称ラムダで同じようなことをするには、総称ラムダをテンプレート変数にします。
#include <iostream>
#include <tuple>
#include <functional>
template <typename T, typename ... U>
auto high_order_fn(T && func, U &&... args) // 高階関数
{
return std::invoke(std::forward<T>(func), std::forward<U>(args)...);
}
// 総称ラムダ(コンパイル時引数付き)
template <auto x>
auto lambda = [](auto&& tup){ std::cout << std::get<x>(tup) << '\n'; };
int main()
{
auto tup = std::make_tuple(1, 3.14, "Hello, World!");
high_order_fn(lambda<2>, tup); // Hello, World!
}
また、C++17 までは、ラムダをテンプレート変数にしてしまうと関数の中に書くことができませんでしたが、C++20 から、総称ラムダのテンプレート構文が追加されたおかげで、関数の中にコンパイル時引数をとるラムダが書けるようになりました。
#include <iostream>
int main()
{
// template <auto x>
// auto l = []{ return x; }; // ここには書けない
auto f = []<auto x>(){ return x; }; // 書ける!
auto a = f.operator()<7>();
}
ラムダのオーバーロードがしたい
boost::hana::overload
を使うと、オーバーロードのようなことができます。次のコードのように、オーバーロードしたいラムダ式を boost::hana::overload
のコンストラクタに渡すことで構築したオブジェクトを使って、関数呼び出しを行います。
#include <boost/hana/functional/overload.hpp>
#include <iostream>
#include <string>
#include <functional>
template <typename Fun, typename ... Args>
auto high_order_fn(Fun&& fn, Args ... args)
{
return std::invoke(std::forward<Fun>(fn), std::forward<Args>(args)...);
}
int main()
{
auto on_string = [](std::string const& s) {
std::cout << "matched std::string: " << s << std::endl;
};
auto on_int = [](int i) {
std::cout << "matched int: " << i << std::endl;
};
auto on_other = [](auto x) {
std::cout << "matched other: " << x << std::endl;
};
auto f = boost::hana::overload(on_int, on_string, on_other);
high_order_fn(f, 3.14); // 出力 : matched other: 3.14
}
[追記]
ラムダのオーバーロードは boost::hana 使えなくても実は簡単に定義できる…https://t.co/Ud3Dz4NJBZ
— omni 鳥頭 (@kariya_mitsuru) December 8, 2019
C++ プログラムを進化させる 4 項目 https://t.co/eCsCfuhYzb #Qiita
Twitter で、kariya さんがラムダのオーバーロードを簡単に定義できる方法を投稿して下さいました。そのコードを紹介させていただきます。
#include <iostream>
#include <string>
#include <functional>
template<typename... Ts>
struct overload : Ts... {
using Ts::operator()...;
};
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;
template <typename Fun, typename ... Args>
auto high_order_fn(Fun&& fn, Args ... args)
{
return std::invoke(std::forward<Fun>(fn), std::forward<Args>(args)...);
}
int main()
{
auto on_string = [](std::string const& s) {
std::cout << "matched std::string: " << s << std::endl;
};
auto on_int = [](int i) {
std::cout << "matched int: " << i << std::endl;
};
auto on_other = [](auto x) {
std::cout << "matched other: " << x << std::endl;
};
auto f = overload{on_int, on_string, on_other};
high_order_fn(f, 3.14); // 出力 : matched other: 3.14
}
C++20 のテンプレートラムダの、テンプレートパラメータを直接指定する呼び出し方
上記にも少し書いたように、C++20 から、総称ラムダにテンプレート構文が追加されたり、評価されない場所(例えば decltype()
のなか)でラムダ式がかけるようになりました。しかし、テンプレートパラメータを直接指定する際にその方法が少し分かりずらいと思われるので、下に例を載せます。ポイントは、テンプレートなのはラムダのクロージャ型(クラス型)そのものではなくて、その operator()
だというところです。
#include <iostream>
#include <boost/type_index.hpp>
using lambda1_t = decltype([]<typename T>()
{
std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
});
auto lambda2 = []<auto x>()
{
std::cout << x;
};
int main()
{
// lambda1_t<int>{}(); ← こうではなくて、次のように書く!
lambda1_t{}.operator()<int>(); // 出力 : int
lambda2.operator()<3>(); // 出力 : 3
}
C++20 から、関数の引数の型に auto
が使えます
また、このトピックとは直接関係ないですが、C++20 から普通の関数の引数に auto
が使えるようになりとても便利になります。例えば、上記のhigh_order_fn
は、次のように書けます。
auto high_order_fn(auto && func, auto &&... args)
{
return std::invoke(std::forward<decltype(func)>(func), std::forward<decltype(args)>(args)...);
}
ただし、gcc で試したところ、総称ラムダで使えていた場所を置き換えることはできないようです。
#include <iostream>
#include <tuple>
#include <functional>
template <typename T, typename ... U>
auto high_order_fn(T && func, U &&... args) // 高階関数
{
return std::invoke(std::forward<T>(func), std::forward<U>(args)...);
}
// 総称ラムダ
auto lambda = [](auto&& x){ std::cout << x << '\n'; };
// 引数の型が auto な関数
void fn(auto&& x)
{
std::cout << x << '\n';
}
int main()
{
high_order_fn(lambda, "Hello, World!");
// high_order_fn(&fn, "Hello, World!"); // エラー!
}
項目 4 : テンプレート引数を取得する
これは嘘です。
— ゆかたゆ (@yukata_yu) November 19, 2019
このままだと全て右辺値になって返ってくるので、こんな感じで微妙なのを書きました。
C++書ける人がスマートにしてくれるはずなのです。https://t.co/ahhPZr0KDx
非型を除くテンプレートパラメータを取得できる、ゆかたゆさんのツイートのコードが Twitter に投稿されていました。とても有用なコードだと思われるので、ここで紹介させていただきます。
[追記]
@yukata_yu https://t.co/WRP7YRcfZU
— いなむのみたまのかみ (@mitama_rs) December 17, 2019
Twitter で、上記のコードを関数テンプレートに対応させたコードが投稿されていました。そこで、そのコードを次に紹介させていただきます。
#include <iostream>
#include <boost/type_index.hpp>
#define typeof(...) boost::typeindex::type_id_with_cvr<__VA_ARGS__>().pretty_name()
// unwrap_helper_idx
template<unsigned int i, class U, class... Args>
struct unwrap_helper_idx_getter: unwrap_helper_idx_getter<i-1, Args...>{};
template<class U, class... Args>
struct unwrap_helper_idx_getter<0, U, Args...>{ using type = U; };
template<unsigned int i, template<class...> class U, class... Args>
auto unwrap_helper_idx_impl(const U<Args...>&) -> unwrap_helper_idx_getter<i, Args...>;
template<unsigned int i, class... Args, class R>
auto unwrap_helper_idx_impl( R(*fn)(Args...) ) -> typename unwrap_helper_idx_getter<i, Args...>::type;
template<unsigned int i, class U>
using unwrap_helper_idx = typename decltype(unwrap_helper_idx_impl<i>(std::declval<U>()))::type;
// tester
template<class A, class B, class C>
struct TestStruct { };
template<class A, class B, class C>
int test_func(A, B, C) {}
int main()
{
TestStruct <int, char&, float&&> val;
std::cout << typeof(unwrap_helper_idx<0, decltype(val)>) << std::endl;
std::cout << typeof(unwrap_helper_idx<1, decltype(val)>) << std::endl;
std::cout << typeof(unwrap_helper_idx<2, decltype(val)>) << std::endl;
std::cout << typeof(decltype(unwrap_helper_idx_impl<2>(test_func<int, char&, float&&>))) << std::endl;
}
int
char&
float&&
float&&
上記のように、エイリアステンプレートunwrap_helper_idx
のテンプレートパラメータの 1 番目に取得したいテンプレート引数の 0 ベースのインデックスを指定し、2 番目にテンプレート引数を取得したいテンプレートを指定すると、取得したい型が得られるようになっているようです。
まとめ
・完全転送のためには、std::forward
にテンプレート引数を指定する必要がある。
・ラムダ式の中で std::move
を使うときは、ラムダを mutable
で修飾する。
・総称ラムダは高階関数の引数に便利。
・非型テンプレート引数がないテンプレートのテンプレート引数を取得することができる。
最後に、ツイートを紹介させていただいためるぽんさん、ゆかたゆさん、kariya さん、いなむのみたまのかみさん、ありがとうございました。
それでは、良い C++20 ライフを!
-
[expr.type]を参照 ↩
-
Effective Modern C++ 項目 28、または右辺値参照~完全転送まで100%理解するぞ! part5 参照の圧縮(reference collapsing) - スーパーハカー幼女 Yuki公主を参照 ↩