21
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C++Advent Calendar 2019

Day 8

C++ プログラムを進化させる 4 項目

Last updated at Posted at 2019-12-07

はじめに

この記事は、C++ Advent Calendar 2019 の 8 日目の投稿です。

昨日は @h6akh さんの C++部・ユニットテストの裏技 〜private関数をテストする〜 でした。
明日は @mdstoy さんの next_combinationを実装してみた になります。

C++20 の機能がどんどん実装されてきていますが、この記事では、C++ 全般の少し使えるかもしれないネタをいくつか紹介しようと思います。タイトルの「C++ プログラムを進化させる〇項目」は、Effective C++ シリーズの副題を流用したものです。4 項目の中身は、小ネタ 2 つと、ツイッターで見つけたネタ 2 つです。また、本文中のサンプルコードでは、読みやすさのために constexprnoexcept などを省略しています。

項目 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_deductionParamTypeに相当する部分は、ケース 2 のユニヴァーサル参照(T&&)です。このとき、次のように T が推論されます。

ケース 2 :

  • exprが左辺値ならば、TParamType も左辺値参照と推論される。これは 2 つの意味で特殊である。まず、テンプレートの型推論で、Tを参照として推論するのはこの場合だけである。もう 1 つは、ParamType の宣言には右辺値参照という形態をとりながら推論される型は左辺値参照となる点である。
  • exprが右辺値の場合は、「通常の」規則が適用される(ケース 1)。

同じく、Effective Modern C++ 第 1 項より

冒頭のコードでは、forward_with_deduction(obj) の関数の引数の式 obj は変数の名前なので左辺値(lvalue)です。したがって、テンプレート引数 T は左辺値参照となることが分かります。式 obj の型は int(もちろん、変数objの型はint&&です。式の場合は最終的に非参照型をもちます。1)なので、結果としてテンプレート引数Tint&と推論されています。

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 では次のように実装されていました。

from_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 の実装の _Tpint になります。また、std::forward<decltype(obj)> のように指定した際は _Tpint&& になります。しかし、_Tp&& はどちらも int&& となるので、テンプレート引数にT,decltype(obj)どちらを指定してもstd::forwardは問題なく動作していることが分かります。

項目 2 : ラムダ式の中でキャプチャした変数を move したいときには、ラムダ式を mutable で修飾する

上記の、めるぽんさんのとても興味深いツイートを紹介します。ラムダ式の中でキャプチャした変数を 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 修飾されており、キャプチャしたオブジェクトを変更することができません。上の例コードでは、関数内ではキャプチャしたオブジェクト aconst がついているようなものだと考えられます。

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
}

[追記]

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 : テンプレート引数を取得する

非型を除くテンプレートパラメータを取得できる、ゆかたゆさんのツイートのコードが Twitter に投稿されていました。とても有用なコードだと思われるので、ここで紹介させていただきます。

[追記]

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 ライフを!

  1. [expr.type]を参照

  2. Effective Modern C++ 項目 28、または右辺値参照~完全転送まで100%理解するぞ! part5 参照の圧縮(reference collapsing) - スーパーハカー幼女 Yuki公主を参照

  3. オーバーロードされた関数へのポインタ / 関数テンプレートへのポインタ - Cry's Blog 参照。コードの例

21
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?