0
1

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.

decltype と ラムダ式 と 私

Last updated at Posted at 2021-02-26

ことの発端

昨日の「template パラメータ内でラムダ式」という記事で decltype が消化できていないので、テストコードを書いて腹落ちさせたい。

更に、std::unique_ptr に副作用がある変数をキャプチャさせた際にどうなるかとコードを書いてみたら、コンパイラが激おこだったので、理由を知りたいという動機です。

# include <iostream>
# include <memory>
int main(void)
{
  const auto deleter1 = [](char* p){ ::free(p); };
  std::unique_ptr<char, decltype(deleter1)> p1(reinterpret_cast<char*>(malloc(1)));

  bool bDelete = false;
  const auto deleter2 = [&bDelete](char* p){ if(bDelete) ::free(p); };
  std::unique_ptr<char, decltype(deleter2)> p2(reinterpret_cast<char*>(malloc(1)));
}
Main.cpp:10:45: error: no matching constructor for initialization of 'std::unique_ptr<char, decltype(deleter2)>' (aka 'unique_ptr<char, const (lambda at Main.cpp:9:25)>')
  std::unique_ptr<char, decltype(deleter2)> p2(reinterpret_cast<char*>(malloc(1)));
                                            ^  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

テストコード1

いきなり難しい事ができないので、まずは簡単なものからやってみる。
decltype(expr) がexprの型を返すのは知っているが、改めてそれを実感してみる。

# include <iostream>
int main(void)
{
  std::string s = "ABC";
  using stype = decltype(s);
  stype ss = "DEF";
  std::cout << typeid(s).name() << std::endl;
  std::cout << typeid(s).name() << std::endl;
}
コンソール出力
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

ここまでは良いとして。

テストコード2

次に本題のラムダ式で decltype を使ってみる。

# include <iostream>
int main(void)
{
  const auto f = [](int a, int b){
      return a + b;
  };
  using ftype = decltype(f);
  ftype ff;
  std::cout << typeid(f).name() << std::endl;
  std::cout << typeid(ff).name() << std::endl;
  std::cout << typeid(decltype(f(0,0))).name() << std::endl;
  std::cout << typeid(decltype(ff(0,0))).name() << std::endl;
}
コンソール出力
Z4mainE3$_0
Z4mainE3$_0
i
i

マングリング化されていて分かりにくいが、ラムダ式によって $_0 という型が定義1され、続けて $_0をインスタンス生成し、そいつが f に代入されると。
ちと、f(0,0) とか f(0,0)int という型になるのは delctype 仕様で理解はしているが、関数型が取得できても良いような気がするが、それは仕様の問題ないし別に腹落ちしない訳じゃないので良しとする。(上から目線でごめんなさい)

テストコード3

で、コンパイラが激おこだったコードに近いのかな?
副作用を持つ関数オブジェクトの場合です。

# include <iostream>
int main(void)
{
  auto bAdd = false;
  const auto f = [&](int a, int b){
      return bAdd ? a + b : a - b;
  };
  using ftype = decltype(f);
//  ftype ff; /* error: no matching constructor for initialization of 'ftype' */
  std::cout << typeid(f).name() << std::endl;
//  std::cout << typeid(ff).name() << std::endl;
  std::cout << typeid(decltype(f(0,0))).name() << std::endl;
}
コンソール出力
Z4mainE3$_0
i

ftype ff; でエラーになります。
そりゃそうです。
キャプチャした変数はクラスオブジェクトのメンバ変数になる訳で、そいつを初期化して生成しないとインスタンス生成なんてできる訳ない。
そして、初期化する術を用意していない。2
コンパイラのエラーは理解したが、このエラーが出るって事は、誰かが生成をしようとしているに違いない。
もう、型と値(オブジェクトインスタンス)が混在して迷子になることが多い。。。

で、「ことの発端」である std::unique_ptr のテンプレートパラメータ仮引数が値ではなく型だということを改めて理解し、decltype を使う理由はここで腹落ちしました。

テストコード4

でも、もう少しやってみます。
次は、ラムダ式の生成と破棄のタイミングを知りたくて3、ラムダ式っぽいことを自作してみます。
(テストコード4では関数オブジェクト型定義まで)

# include <iostream>
# include <memory>
int main(void)
{
  struct deleter_t {
    deleter_t() { std::cout << "deleter_t::ctor()" << std::endl; }
    ~deleter_t() { std::cout << "deleter_t::dtor()" << std::endl; }
    void operator () (char* p) const { std::cout << "deleter_t::operator()" << std::endl; ::free(p); }
  };
  
  std::cout << "scope in" << std::endl;
  {
    std::unique_ptr<char, deleter_t> p(reinterpret_cast<char*>(malloc(1)));
  }
  std::cout << "scope out" << std::endl;
}
コンソール出力
scope in
deleter_t::ctor()
deleter_t::operator()
deleter_t::dtor()
scope out

ここまでくると、ふむふむって感じで、ようやく気持ちが穏やかな世界に戻れました。
余談ですが、operator () constをいつもの癖で付加していたけど、ラムダ式でこの const属性を外すのが mutable という仕様も理解しました。4

テストコード5

「テストコード4」の続きで decltype が使われて機能している感じを知りたく、さらにラムダ式に寄せるべく、クラスオブジェクトの生成を行います。

# include <iostream>
# include <memory>
int main(void)
{
  struct deleter_t {
    deleter_t() { std::cout << "deleter_t::ctor()" << std::endl; }
    ~deleter_t() { std::cout << "deleter_t::dtor()" << std::endl; }
    void operator () (char* p) const { std::cout << "deleter_t::operator()" << std::endl; ::free(p); }
  };

  const auto deleter = deleter_t();

  /* 下記のラムダ式は概ね上記の「型定義」と「オブジェクト生成」をまとめた糖衣構文 */
  // const auto deleter = [](char* p){ ::free(p); };

  std::cout << "scope in" << std::endl;
  {
    std::unique_ptr<char, decltype(deleter)> p(reinterpret_cast<char*>(malloc(1)));
  }
  std::cout << "scope out" << std::endl;
}
コンソール出力
deleter_t::ctor()
scope in
deleter_t::ctor()
deleter_t::operator()
deleter_t::dtor()
scope out
deleter_t::dtor()

ここにきて、そうか、ラムダ式で得られたオブジェクトをdecltypeするんだから、無駄な生成と破棄が多く発生するのか。。。
いや、もう凡人はここまでで良いでしょう。
ようやく、これで、ゆっくり眠れそう。

ふと

using deleter_t = decltype([](char* p){::free(p);});
std::unique_ptr<char, decltype([](char* p){::free(p);})> p(reinterpret_cast<char*>(malloc(1)));

こんなこと考えて、「template パラメータ内でラムダ式」にループしそう。。。

  1. ストラウストラップ氏著「プログラミング言語C++」の「§11.4.5 ラムダ式の型」によると、ラムダ式の型は定義されず closure type と呼ばれるラムダ式特有の型になるとしている。

  2. どこかに明確な記述があった気がするけど、探しきれていない。

  3. これ、調べる術って無いのかな? Reactive Extension 使っていると never, retry とか、 completed や、 unsibscribe し忘れで、結構問題になるんだよな。。。

  4. ストラウストラップ氏著「プログラミング言語C++」「§11.4.1 実装モデル」

0
1
2

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?